← toolkit.bot

PDF to EPUB Conversion in Ruby — toolkit.bot API Integration

June 12, 2026  ·  8 min read

This guide shows how to call the toolkit.bot REST API from Ruby to convert PDF files to EPUB. The API accepts a PDF via multipart upload, processes it asynchronously, and returns a download URL when the job is complete.

Net::HTTP — no dependencies

The standard library net/http handles multipart uploads with a bit of manual boundary construction:

require 'net/http'
require 'json'

API_BASE = 'https://toolkit.bot/api/v1'
API_KEY  = ENV.fetch('TOOLKIT_API_KEY')

def convert_pdf_to_epub(pdf_path, output_path)
  # Upload
  uri      = URI("#{API_BASE}/jobs")
  boundary = "RubyBoundary#{Time.now.to_i}"
  body     = build_multipart(pdf_path, boundary)

  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Bearer #{API_KEY}"
  req['Content-Type']  = "multipart/form-data; boundary=#{boundary}"
  req.body = body

  resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
  raise "Upload failed: #{resp.body}" unless resp.is_a?(Net::HTTPSuccess)

  job_id = JSON.parse(resp.body)['job_id']
  puts "Job ID: #{job_id}"

  # Poll
  download_url = nil
  60.times do
    sleep 4
    status_uri = URI("#{API_BASE}/jobs/#{job_id}")
    status_req = Net::HTTP::Get.new(status_uri)
    status_req['Authorization'] = "Bearer #{API_KEY}"
    status_resp = Net::HTTP.start(status_uri.host, status_uri.port, use_ssl: true) do |h|
      h.request(status_req)
    end
    data = JSON.parse(status_resp.body)
    puts "Status: #{data['status']}"
    if data['status'] == 'done'
      download_url = data['download_url']
      break
    end
    raise "Conversion failed: #{data.inspect}" if data['status'] == 'failed'
  end
  raise 'Timed out' unless download_url

  # Download
  dl_uri = URI(download_url)
  dl_req = Net::HTTP::Get.new(dl_uri)
  dl_req['Authorization'] = "Bearer #{API_KEY}"
  epub = Net::HTTP.start(dl_uri.host, dl_uri.port, use_ssl: true) { |h| h.request(dl_req) }
  File.binwrite(output_path, epub.body)
  puts "Saved: #{output_path}"
end

def build_multipart(path, boundary)
  filename = File.basename(path)
  content  = File.binread(path)
  [
    "--#{boundary}
",
    "Content-Disposition: form-data; name="file"; filename="#{filename}"
",
    "Content-Type: application/pdf

",
    content,
    "
--#{boundary}--
"
  ].join
end

convert_pdf_to_epub('document.pdf', 'document.epub')

Faraday example

If Faraday is already in your Gemfile, the multipart handling is much cleaner:

# Gemfile
gem 'faraday'
gem 'faraday-multipart'
require 'faraday'
require 'faraday/multipart'
require 'json'

API_BASE = 'https://toolkit.bot/api/v1'
API_KEY  = ENV.fetch('TOOLKIT_API_KEY')

conn = Faraday.new(url: API_BASE) do |f|
  f.request :multipart
  f.request :url_encoded
  f.adapter Faraday.default_adapter
end

# Upload
resp = conn.post('/jobs') do |req|
  req.headers['Authorization'] = "Bearer #{API_KEY}"
  req.body = { file: Faraday::UploadIO.new('document.pdf', 'application/pdf') }
end
job_id = JSON.parse(resp.body)['job_id']
puts "Job: #{job_id}"

# Poll
download_url = nil
60.times do
  sleep 4
  status = conn.get("/jobs/#{job_id}") do |req|
    req.headers['Authorization'] = "Bearer #{API_KEY}"
  end
  data = JSON.parse(status.body)
  break (download_url = data['download_url']) if data['status'] == 'done'
  raise data.inspect if data['status'] == 'failed'
end

# Download
epub = Faraday.get(download_url, nil, 'Authorization' => "Bearer #{API_KEY}")
File.binwrite('document.epub', epub.body)
puts 'Done: document.epub'

Rails integration

In a Rails app, wrap the conversion in an ActiveJob to avoid blocking web requests:

# app/jobs/epub_conversion_job.rb
class EpubConversionJob < ApplicationJob
  queue_as :default

  def perform(document_id)
    doc = Document.find(document_id)

    conn = Faraday.new(url: 'https://toolkit.bot/api/v1') do |f|
      f.request :multipart
      f.adapter :net_http
    end

    # Upload
    resp = conn.post('/jobs') do |req|
      req.headers['Authorization'] = "Bearer #{ENV['TOOLKIT_API_KEY']}"
      req.body = { file: Faraday::UploadIO.new(doc.pdf_path, 'application/pdf') }
    end
    job_id = JSON.parse(resp.body)['job_id']

    # Poll (use retries via Sidekiq or manual loop)
    60.times do
      sleep 5
      status_resp = conn.get("/jobs/#{job_id}") do |req|
        req.headers['Authorization'] = "Bearer #{ENV['TOOLKIT_API_KEY']}"
      end
      data = JSON.parse(status_resp.body)
      if data['status'] == 'done'
        epub = Faraday.get(data['download_url'],
          nil, 'Authorization' => "Bearer #{ENV['TOOLKIT_API_KEY']}")
        doc.update!(epub_data: epub.body, epub_ready: true)
        return
      end
      raise "failed" if data['status'] == 'failed'
    end
  end
end

Call it from a controller: EpubConversionJob.perform_later(document.id)

FAQ

Does this work with Ruby 2.x?

Yes. Net::HTTP and Faraday support Ruby 2.7+. The examples above use no Ruby 3-specific syntax.

How do I handle Faraday SSL errors?

Add f.ssl.verify = true in the connection block. If you see SSL errors in development, install the certifi gem or set ssl_verify_peer: false (never in production).

Is there a Ruby gem for toolkit.bot?

Not yet. The REST API is simple enough that the Faraday wrapper above is production-ready without a dedicated gem.

Can I use Typhoeus for parallel requests?

Yes. Use Typhoeus for parallel uploads in batch workflows — submit all jobs first, then poll them in a single loop. The API supports concurrent job submissions.

Get your free API key
Sign up at toolkit.bot/api — free tier available, no credit card required.