Async Rust SDK primitives for the CloudConvert API v2.
The crate is built for Tokio applications that need to create CloudConvert jobs,
upload files, wait for results, download export/url outputs, inspect operation
metadata, verify webhooks, or use OAuth access tokens without hand-building
every request.
This is an unofficial library. For service behavior, scopes, formats, engines, regions, sandbox usage, and operation-specific options, use the official CloudConvert API documentation and the CloudConvert Job Builder.
[dependencies]
cloudconvert-sdk = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The crate is a library only. It does not install a binary.
Create an API key in the CloudConvert dashboard and export it:
export CLOUDCONVERT_API_KEY=...Then build a job, wait for it, and download the export/url result:
use cloudconvert_sdk::{ApiKey, CloudConvertClient, FileExtension, JobCreateRequest};
#[tokio::main]
async fn main() -> cloudconvert_sdk::Result<()> {
let client = CloudConvertClient::builder(ApiKey::from_env()?).build()?;
let request = JobCreateRequest::linear()
.import_url("https://example.test/input.docx")
.convert(FileExtension::Pdf)
.export_url()
.build();
let job = client.jobs().create(request).await?;
let finished = client.jobs().wait(&job.id).await?;
for file in finished.export_urls() {
if let Some(url) = &file.url {
let bytes = client.download(url).await?;
println!("downloaded {} bytes as {}", bytes.len(), file.filename);
}
}
Ok(())
}For production workflows, prefer CloudConvert webhooks or Socket.io waits over long blocking waits.
CloudConvert jobs serialize tasks as an object keyed by task name. The SDK generates those names unless you choose to provide explicit names.
Use JobCreateRequest::linear() when every task feeds into the next task:
use cloudconvert_sdk::{FileExtension, JobCreateRequest};
let request = JobCreateRequest::linear()
.import_url("https://example.test/input.docx")
.convert(FileExtension::Pdf)
.export_url()
.build();Use *_with(...) methods when a task needs options but the job is still
serial:
let request = JobCreateRequest::linear()
.import_url_with("https://example.test/input.docx", |task| {
task.filename("input.docx")
})
.convert_with(FileExtension::Pdf, |task| {
task.input_format(FileExtension::Docx)
.engine("office")
.filename("converted.pdf")
})
.export_url_with(|task| task.inline(false))
.build();Use JobCreateRequest::graph(|job| ...) when a job branches, joins multiple
inputs, or needs to reference a non-adjacent task. Each graph method returns a
TaskName handle.
use cloudconvert_sdk::{FileExtension, JobCreateRequest};
let request = JobCreateRequest::graph(|job| {
let source = job.import_url("https://example.test/input.docx");
let pdf = job.convert(&source, FileExtension::Pdf);
let png = job.convert(&source, FileExtension::Png);
job.export_url([&pdf, &png]);
})
.tag("branch-demo")
.build();TaskName handles also work for multi-input operations such as merge,
watermarks that use an imported image file, and export/url tasks that archive
multiple outputs:
use cloudconvert_sdk::{
FileExtension, JobCreateRequest, Layer, PositionHorizontal, PositionVertical,
};
let request = JobCreateRequest::graph(|job| {
let cover_docx = job.import_url_with("https://example.test/report-cover.docx", |task| {
task.filename("cover.docx")
});
let body_docx = job.import_url_with("https://example.test/report-body.docx", |task| {
task.filename("body.docx")
});
let logo_png = job.import_url_with("https://example.test/logo.png", |task| {
task.filename("logo.png")
});
let cover_pdf = job.convert_with(&cover_docx, FileExtension::Pdf, |task| {
task.input_format(FileExtension::Docx).filename("cover.pdf")
});
let body_pdf = job.convert_with(&body_docx, FileExtension::Pdf, |task| {
task.input_format(FileExtension::Docx).filename("body.pdf")
});
let merged = job.merge_with([&cover_pdf, &body_pdf], FileExtension::Pdf, |task| {
task.filename("report.pdf")
});
let watermarked = job.watermark_image_with(&merged, &logo_png, |task| {
task.input_format(FileExtension::Pdf)
.layer(Layer::Above)
.image_width(180)
.position(PositionVertical::Bottom, PositionHorizontal::Right)
.margins(24, 24)
.opacity(80)
.filename("report-watermarked.pdf")
});
job.export_url_with([&cover_pdf, &body_pdf, &watermarked], |task| {
task.archive_multiple_files(true)
});
})
.tag("report-package")
.build();When the task name itself matters, use JobBuilder::task(...),
JobBuilder::add_named_task(...), or JobGraphBuilder::add_named_task(...).
For operations not yet typed by the SDK, use TaskRequest::custom(...).
Use FileExtension for known CloudConvert format tokens:
use cloudconvert_sdk::{ConvertTask, FileExtension};
let task = ConvertTask::new("upload-file", FileExtension::Pdf)
.input_format(FileExtension::Docx);Format setters still accept strings for forward compatibility. Strings are
normalized by trimming leading dots and lowercasing ASCII, so .PDF and PDF
serialize as pdf.
Use import/upload when your application already has the input file locally.
The job creation response contains the signed upload form; the SDK handles the
multipart upload helper.
use std::path::Path;
use cloudconvert_sdk::{ApiKey, CloudConvertClient, FileExtension, JobCreateRequest};
async fn run() -> cloudconvert_sdk::Result<()> {
let client = CloudConvertClient::builder(ApiKey::from_env()?).build()?;
let request = JobCreateRequest::linear()
.import_upload()
.convert_with(FileExtension::Pdf, |task| {
task.input_format(FileExtension::Txt)
})
.export_url()
.build();
let job = client.jobs().create(request).await?;
let upload_task_id = job
.tasks
.iter()
.find(|task| task.operation == "import/upload")
.and_then(|task| task.id.as_deref())
.expect("import/upload task should have an id");
let upload_task = client.tasks().get(upload_task_id).await?;
client.upload_path(&upload_task, "input.txt").await?;
let finished = client.jobs().wait(&job.id).await?;
for file in finished.export_urls() {
if let Some(url) = &file.url {
let filename = Path::new(&file.filename)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("download");
client.download_to_path(url, Path::new("downloads").join(filename)).await?;
}
}
Ok(())
}Download helpers never attach CloudConvert bearer credentials to signed storage URLs. Upload helpers submit to the signed form action returned by CloudConvert.
The crate exports typed resource clients from CloudConvertClient:
jobs()creates, lists, fetches, waits for, redirects, and deletes jobs.tasks()creates standalone tasks, lists, fetches, waits for, cancels, retries, and deletes tasks.operations()lists operation metadata, options, engine versions, and can validate task payloads against returned metadata.users()reads the authenticated user.webhooks()creates, lists, and deletes webhooks.
Useful helpers:
download(...),download_stream(...), anddownload_to_path(...).upload_bytes(...),upload_body(...),upload_stream(...), andupload_path(...).sign_payload(...)andverify_signature(...)for webhook signatures.sign_job_url(...)for signed job-template URLs.socket_base_url(...),SocketChannel,JobSocketEvent, andTaskSocketEventfor Socket.io payloads.
Client setup supports API keys, OAuth access tokens, sandbox mode, custom
regions, custom base URLs, custom reqwest clients, transport timeouts, and the
optional retry and socket features.
Use API keys for server-side integrations owned by one CloudConvert account. Use OAuth when your app acts on behalf of CloudConvert users.
use cloudconvert_sdk::{
JobListQuery, OAuthClient, OAuthClientSecret, OAuthScope,
};
async fn run() -> cloudconvert_sdk::Result<()> {
let oauth = OAuthClient::new("client-id", OAuthClientSecret::new("client-secret"))?;
let redirect = oauth.authorization_code_url_with_state(
"https://app.example.test/cloudconvert/callback",
[OAuthScope::TaskRead, OAuthScope::TaskWrite],
"state-from-your-app",
)?;
// Redirect the user to `redirect`, then exchange the returned code.
let token = oauth
.exchange_code("authorization-code", "https://app.example.test/cloudconvert/callback")
.await?;
let client = token.into_client_builder().build()?;
let _jobs = client.jobs().list(&JobListQuery::default()).await?;
Ok(())
}OAuthAccessToken, OAuthRefreshToken, and OAuthClientSecret redact debug
output. OAuth-backed clients use the same SDK resources and Socket.io helpers as
API-key clients.
For metadata-driven integrations, call operations().list(...) with
include_options() or include_options_and_engine_versions():
use cloudconvert_sdk::{ConvertTask, OperationListQuery, TaskRequest};
async fn validate(client: cloudconvert_sdk::CloudConvertClient) -> cloudconvert_sdk::Result<()> {
let operation = client.operations().list(
&OperationListQuery::default()
.operation("convert")
.input_format("docx")
.output_format("pdf")
.include_options_and_engine_versions(),
).await?.remove(0);
let task = TaskRequest::from(ConvertTask::new("import-file", "pdf"));
operation.validate_task(&task).expect("task should match operation metadata");
Ok(())
}Use option(...) builder methods, extra maps, or TaskRequest::custom(...)
for operation-specific options that are not yet typed by this SDK.
Automatic retry is off by default. Enable the optional feature and set a policy:
cloudconvert-sdk = { version = "0.1", features = ["retry"] }use std::time::Duration;
use cloudconvert_sdk::{ApiKey, CloudConvertClient, RetryPolicy, TransportConfig};
let client = CloudConvertClient::builder(ApiKey::from_env()?)
.transport_config(
TransportConfig::default()
.connect_timeout(Duration::from_secs(10))
.request_timeout(Duration::from_secs(120)),
)
.retry_policy(
RetryPolicy::new(3)
.initial_delay(Duration::from_millis(250))
.max_delay(Duration::from_secs(10)),
)
.build()?;Retry covers CloudConvert API and synchronous API requests for transient
statuses 429, 500, 502, 503, and 504, plus connect and timeout
errors. Signed import/upload form submissions and export/url downloads stay
outside that retry boundary.
Enable the optional feature when an async application wants lower-latency completion than polling and does not expose a public webhook receiver:
cloudconvert-sdk = { version = "0.1", features = ["socket"] }use cloudconvert_sdk::{ApiKey, CloudConvertClient, FileExtension, JobCreateRequest};
async fn run() -> cloudconvert_sdk::Result<()> {
let client = CloudConvertClient::builder(ApiKey::from_env()?).build()?;
let request = JobCreateRequest::linear()
.import_url("https://example.test/input.docx")
.convert(FileExtension::Pdf)
.export_url()
.build();
let finished = client.jobs().create_and_wait_socket(request).await?;
for file in finished.export_urls() {
println!("{}", file.filename);
}
Ok(())
}The managed wait helpers subscribe, check the current resource state to avoid missing fast completions, wait for a terminal Socket.io event, and disconnect. Use webhooks when CloudConvert can call your service directly.
For streams, use client.socket(...) with SocketChannel,
jobs().task_events_socket(job_id), or users().events_socket().
These examples build request payloads and print JSON. They do not call the live CloudConvert API, so they are safe to run without credentials:
cargo run --example build_job
cargo run --example linear_options_job
cargo run --example branch_job
cargo run --example advanced_job
cargo run --example file_extensionscargo fmt --all -- --check
cargo check --workspace --all-targets --locked
cargo check --workspace --all-targets --all-features --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-targets --locked
cargo test --workspace --all-targets --all-features --lockedCI also generates an llvm-cov HTML coverage artifact and enforces the current
coverage threshold.
Live CloudConvert tests are ignored by default so normal CI and cargo test do
not consume API credits.
Put a real key in .env or the process environment:
CLOUDCONVERT_API_KEY=...Run the live group explicitly:
cargo test --test live_api -- --ignoredThe live group keeps API usage small. It creates and deletes live tasks/jobs,
including a watermark job shape, and has one ignored upload/convert/export flow
with a tiny generated text file. It needs task/job scopes, not user.read.