PDF to EPUB Conversion in Rust — toolkit.bot API Integration
This guide shows how to call the toolkit.bot REST API from Rust to convert PDF files to EPUB. The implementation uses reqwest for HTTP and tokio for async runtime — both standard choices in the Rust ecosystem.
Dependencies
Add to Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["multipart", "json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Complete async example
use reqwest::{Client, multipart};
use serde::Deserialize;
use std::{env, path::Path, time::Duration};
use tokio::{fs, time};
#[derive(Deserialize, Debug)]
struct JobResponse {
job_id: Option<String>,
status: Option<String>,
download_url: Option<String>,
}
const API_BASE: &str = "https://toolkit.bot/api/v1";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = env::var("TOOLKIT_API_KEY")?;
let pdf_path = env::args().nth(1).expect("Usage: converter input.pdf");
let out_path = pdf_path.replace(".pdf", ".epub");
convert_pdf_to_epub(&api_key, &pdf_path, &out_path).await?;
println!("Saved: {out_path}");
Ok(())
}
async fn convert_pdf_to_epub(
api_key: &str,
pdf_path: &str,
out_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
// --- Upload ---
let file_bytes = fs::read(pdf_path).await?;
let filename = Path::new(pdf_path)
.file_name().unwrap().to_string_lossy().into_owned();
let part = multipart::Part::bytes(file_bytes)
.file_name(filename)
.mime_str("application/pdf")?;
let form = multipart::Form::new().part("file", part);
let upload_resp: JobResponse = client
.post(format!("{API_BASE}/jobs"))
.bearer_auth(api_key)
.multipart(form)
.send().await?
.json().await?;
let job_id = upload_resp.job_id.expect("Missing job_id in response");
println!("Job ID: {job_id}");
// --- Poll ---
let download_url = loop {
time::sleep(Duration::from_secs(4)).await;
let status: JobResponse = client
.get(format!("{API_BASE}/jobs/{job_id}"))
.bearer_auth(api_key)
.send().await?
.json().await?;
println!("Status: {:?}", status.status);
match status.status.as_deref() {
Some("done") => break status.download_url.expect("Missing download_url"),
Some("failed") => return Err("Conversion failed".into()),
_ => continue,
}
};
// --- Download ---
let epub_bytes = client
.get(&download_url)
.bearer_auth(api_key)
.send().await?
.bytes().await?;
fs::write(out_path, &epub_bytes).await?;
Ok(())
}
Build and run:
cargo build --release
TOOLKIT_API_KEY=tk_xxx ./target/release/converter document.pdf
Synchronous variant
If you prefer blocking I/O (e.g., in a CLI tool where async adds complexity), use reqwest's blocking client:
[dependencies]
reqwest = { version = "0.12", features = ["multipart", "json", "blocking"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use reqwest::blocking::{Client, multipart};
fn convert_pdf_to_epub(api_key: &str, pdf_path: &str, out_path: &str)
-> Result<(), Box<dyn std::error::Error>>
{
let client = Client::new();
let file = std::fs::read(pdf_path)?;
let part = multipart::Part::bytes(file)
.file_name("document.pdf")
.mime_str("application/pdf")?;
let form = multipart::Form::new().part("file", part);
let upload: serde_json::Value = client
.post("https://toolkit.bot/api/v1/jobs")
.bearer_auth(api_key)
.multipart(form)
.send()?.json()?;
let job_id = upload["job_id"].as_str().unwrap().to_string();
loop {
std::thread::sleep(std::time::Duration::from_secs(4));
let status: serde_json::Value = client
.get(format!("https://toolkit.bot/api/v1/jobs/{job_id}"))
.bearer_auth(api_key)
.send()?.json()?;
match status["status"].as_str() {
Some("done") => {
let url = status["download_url"].as_str().unwrap();
let epub = client.get(url).bearer_auth(api_key).send()?.bytes()?;
std::fs::write(out_path, &epub)?;
return Ok(());
}
Some("failed") => return Err("failed".into()),
_ => {}
}
}
}
Error handling
For production use, replace Box<dyn std::error::Error> with a typed error enum using thiserror:
[dependencies]
thiserror = "1"
#[derive(thiserror::Error, Debug)]
enum ConversionError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Conversion failed on server")]
ServerFailed,
#[error("Timed out waiting for job to complete")]
Timeout,
#[error("Environment variable not set: {0}")]
Env(#[from] std::env::VarError),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
FAQ
Which async runtime should I use?
Tokio is the standard choice and what reqwest defaults to. If your project uses async-std, feature-flag reqwest with default-features = false, features = ["async-std-runtime", ...].
How do I add a timeout to the polling loop?
Use tokio::time::timeout around the poll loop:
let download_url = tokio::time::timeout(
Duration::from_secs(300),
poll_for_completion(&client, api_key, &job_id),
).await??;
Is there a Rust crate for toolkit.bot?
Not yet — the REST API is simple enough that the implementations above are production-ready. Consider wrapping them in your own crate if you use the API across multiple projects.
Sign up at toolkit.bot/api — free tier, no credit card required.