Introduction

The Convert.Online API lets you convert files programmatically. You describe a job as a small graph of tasks — import a file, convert it, export a download link — and the API runs it asynchronously and hands you back the result.

The base URL for all endpoints is:

https://api.convert.online

Requests and responses are JSON. Every call that touches your account is authenticated with an API key (see Authentication). The fastest way to see a working request is the visual Job Builder, which generates ready-to-run code in eight languages as you build the job.

The flow at a glance

  1. Create a job (POST /v1/process/jobs). For uploaded files you get a signed upload URL back.
  2. Upload your file to that URL.
  3. Start the conversion (complete-upload).
  4. Poll the job (or listen for events) until it is finished.
  5. Download the result from the export task's URL.

Authentication

Authenticate every request with an API key from your dashboard (Dashboard → API Keys). A key is shown once when you create it — store it securely; we only keep a hash.

Send the key as a Bearer token:

Authorization: Bearer <your-api-key>

The header X-API-Key: <your-api-key> is also accepted. Requests without a valid key return 401. If a key is restricted to specific IPs and the request comes from another address, it returns 403.

A request with your key

                                    req, _ := http.NewRequest("GET", "https://api.convert.online/v1/process/jobs/{JOB_ID}", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
                                
                                    GET /v1/process/jobs/{JOB_ID} HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}"))
    .header("Authorization", "Bearer {API_KEY}")
    .GET()
    .build();

HttpResponse<String> res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}", {
  headers: { "Authorization": "Bearer {API_KEY}" }
});
const job = await res.json();
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

job = requests.get(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}",
    headers={"Authorization": "Bearer {API_KEY}"},
).json()
                                
                                    require "net/http"
require "json"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer {API_KEY}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl https://api.convert.online/v1/process/jobs/{JOB_ID} \
  -H 'Authorization: Bearer {API_KEY}'
                                

Rate limits & quota

Each API key is limited to 60 requests per minute. Exceeding it returns 429 with a retry_after hint.

Conversions consume your account's storage/transfer quota (measured in GB): the source file size is metered when a job runs, and re-downloading a finished file consumes quota too (the first download of each file is free). When the quota is exhausted, job creation and downloads return 402. Registered free accounts include a monthly allowance; paid plans raise it.

Your plan also sets how many conversions may run at the same time, and a single job may not ask for more than that: a plan allowing 5 accepts a job with up to 5 convert tasks, and answers 422 to a job with 6, telling you the allowance and how many you asked for. Send the rest in another job. Paid plans have no such cap — a job may carry up to 100 convert tasks, and they run as server capacity allows.

Tip: use the Sandbox to develop and test integrations without touching your quota.

Core concepts

Jobs

A job is a collection of tasks that run as one unit. You submit the whole graph in a single request and track it by its id.

Tasks

Each task has an operation and, usually, an input that wires it to a previous task. The supported operations are:

OperationPurpose
import/uploadProvisions a signed URL you upload your file to.
import/urlWe fetch the source from a public URL you provide.
import/base64You embed the source bytes as base64 in the request.
convertConverts its input to output_format.
export/urlProduces a download link for its input's result.
export/webhookPOSTs a signed event to your URL when the job finishes.
import/s3, import/sftp, import/googlecloudPull the source from your own cloud storage — see Cloud storage.
export/s3, export/sftp, export/googlecloudPush the result to your own cloud storage — see Cloud storage.

Wiring & chaining

A convert task's input is the name of an import task; an export task's input is an array of convert-task names. A convert's input can also be another convert, so you can chain conversions (A → B → C). See Chained conversions.

Quick start

Convert a JPG to a PNG end to end. Replace {API_KEY} with your key.

1. Create the job

You get back a job id and, on the import-1 task, an upload form (URL + headers).

2. Upload your file

PUT the file bytes to result.form.url using the returned headers.

3. Start the conversion

Call complete-upload to enqueue the work.

4. Poll until finished

GET the job until status is finished (or failed).

5. Download

The export-1 task's result.url is your download link.

1 · Create the job

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1": { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
      "input_format": "jpg", "output_format": "png" },
    "export-1": { "operation": "export/url", "input": ["convert-1"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1": { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
      "input_format": "jpg", "output_format": "png" },
    "export-1": { "operation": "export/url", "input": ["convert-1"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1": { "operation": "import/upload" },
                "convert-1": { "operation": "convert", "input": "import-1",
                  "input_format": "jpg", "output_format": "png" },
                "export-1": { "operation": "export/url", "input": ["convert-1"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: JSON.stringify({
    tasks: {
      "import-1": { operation: "import/upload" },
      "convert-1": { operation: "convert", input: "import-1",
        input_format: "jpg", output_format: "png" },
      "export-1": { operation: "export/url", input: ["convert-1"] }
    }
  })
});
const job = await res.json();
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "tasks" => [
            "import-1"  => ["operation" => "import/upload"],
            "convert-1" => ["operation" => "convert", "input" => "import-1",
                            "input_format" => "jpg", "output_format" => "png"],
            "export-1"  => ["operation" => "export/url", "input" => ["convert-1"]],
        ],
    ]),
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Authorization": "Bearer {API_KEY}"},
    json={
        "tasks": {
            "import-1": {"operation": "import/upload"},
            "convert-1": {"operation": "convert", "input": "import-1",
                          "input_format": "jpg", "output_format": "png"},
            "export-1": {"operation": "export/url", "input": ["convert-1"]},
        }
    },
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = {
  tasks: {
    "import-1"  => { operation: "import/upload" },
    "convert-1" => { operation: "convert", input: "import-1",
                     input_format: "jpg", output_format: "png" },
    "export-1"  => { operation: "export/url", input: ["convert-1"] }
  }
}.to_json

job = JSON.parse(http.request(req).body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
    "tasks": {
      "import-1": { "operation": "import/upload" },
      "convert-1": { "operation": "convert", "input": "import-1",
        "input_format": "jpg", "output_format": "png" },
      "export-1": { "operation": "export/url", "input": ["convert-1"] }
    }
  }'
                                

2 · Upload the file

                                    // form = the import task's result.form from the create response
data, _ := os.ReadFile("photo.jpg")
req, _ := http.NewRequest("PUT", form.URL, bytes.NewReader(data))
req.Header.Set("Content-Type", "application/octet-stream")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
resp.Body.Close()
                                
                                    PUT /…signed-put-url… HTTP/1.1
Host: storage.googleapis.com
Content-Type: application/octet-stream

<binary file bytes>
                                
                                    // formUrl = the import task's result.form.url from the create response
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(formUrl))
    .header("Content-Type", "application/octet-stream")
    .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("photo.jpg")))
    .build();

HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
                                
                                    // form = the import task's result.form from the create response
await fetch(form.url, {
  method: form.method,     // "PUT"
  headers: form.headers,   // { "Content-Type": "application/octet-stream" }
  body: fileBlob           // a File / Blob / ArrayBuffer
});
                                
                                    <?php
// $form = the import task's result.form from the create response
$ch = curl_init($form["url"]);
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "PUT",
    CURLOPT_POSTFIELDS => file_get_contents("photo.jpg"),
    CURLOPT_HTTPHEADER => ["Content-Type: application/octet-stream"],
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
                                
                                    import requests

# form = the import task's result.form from the create response
with open("photo.jpg", "rb") as f:
    requests.put(form["url"], data=f, headers=form["headers"])
                                
                                    require "net/http"
require "uri"

# form = the import task's result.form from the create response
uri = URI(form["url"])
req = Net::HTTP::Put.new(uri)
req["Content-Type"] = "application/octet-stream"
req.body = File.binread("photo.jpg")

Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
                                
                                    # Use the form.url + form.headers from the import task's result
curl -X PUT "https://storage.googleapis.com/…signed-put-url…" \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @photo.jpg
                                

3 · Start the conversion

                                    req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
resp.Body.Close()
                                
                                    POST /v1/process/jobs/{JOB_ID}/complete-upload HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload"))
    .header("Authorization", "Bearer {API_KEY}")
    .POST(HttpRequest.BodyPublishers.noBody())
    .build();

HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
                                
                                    await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload", {
  method: "POST",
  headers: { "Authorization": "Bearer {API_KEY}" }
});
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
curl_exec($ch);
                                
                                    import requests

requests.post(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload",
    headers={"Authorization": "Bearer {API_KEY}"},
)
                                
                                    require "net/http"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer {API_KEY}"

Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload \
  -H 'Authorization: Bearer {API_KEY}'
                                

4 · Poll the job

                                    req, _ := http.NewRequest("GET", "https://api.convert.online/v1/process/jobs/{JOB_ID}", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
                                
                                    GET /v1/process/jobs/{JOB_ID} HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}"))
    .header("Authorization", "Bearer {API_KEY}")
    .GET()
    .build();

HttpResponse<String> res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}", {
  headers: { "Authorization": "Bearer {API_KEY}" }
});
const job = await res.json();
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

job = requests.get(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}",
    headers={"Authorization": "Bearer {API_KEY}"},
).json()
                                
                                    require "net/http"
require "json"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer {API_KEY}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl https://api.convert.online/v1/process/jobs/{JOB_ID} \
  -H 'Authorization: Bearer {API_KEY}'
                                

Finished job

HTTP/1.1 200 OK

{
  "id": "b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6",
  "status": "finished",
  "createdAt": "2026-07-20T10:00:00+00:00",
  "endedAt": "2026-07-20T10:00:07+00:00",
  "tasks": [
    { "name": "convert-1", "operation": "convert", "status": "finished" },
    {
      "name": "export-1", "operation": "export/url", "status": "finished",
      "result": {
        "url": "https://api.convert.online/v1/download/eyJ…token…",
        "filename": "photo.png"
      }
    }
  ],
  "links": { "self": "/v1/process/jobs/b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6" }
}

5 · Download

                                    req, _ := http.NewRequest("GET", "https://api.convert.online/v1/download/eyJ…token…", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

out, _ := os.Create("photo.png")
defer out.Close()
io.Copy(out, resp.Body)
                                
                                    GET /v1/download/eyJ…token… HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/download/eyJ…token…"))
    .header("Authorization", "Bearer {API_KEY}")
    .build();

client.send(req, HttpResponse.BodyHandlers.ofFile(Path.of("photo.png")));
                                
                                    const res = await fetch("https://api.convert.online/v1/download/eyJ…token…", {
  headers: { "Authorization": "Bearer {API_KEY}" }
});
const blob = await res.blob(); // save/stream as needed
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/download/eyJ…token…");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
file_put_contents("photo.png", curl_exec($ch));
                                
                                    import requests

# The export result.url is a ready-to-use link (redirects to storage).
r = requests.get("https://api.convert.online/v1/download/eyJ…token…",
                 headers={"Authorization": "Bearer {API_KEY}"}, allow_redirects=True)
with open("photo.png", "wb") as f:
    f.write(r.content)
                                
                                    require "open-uri"

URI.open("https://api.convert.online/v1/download/eyJ…token…",
         "Authorization" => "Bearer {API_KEY}") do |src|
  File.binwrite("photo.png", src.read)
end
                                
                                    # The export result.url is already a full, ready-to-use link:
curl -L "https://api.convert.online/v1/download/eyJ…token…" -o photo.png
                                

Create a job

POST /v1/process/jobs

Submit the job graph as a tasks object. For every import/upload task, the response includes a signed upload form. The job starts in waiting_upload.

FieldTypeRequiredDescription
tasksobjectyesMap of task name → task definition.
tagstringnoYour own label, echoed back on the job.
sandboxbooleannoSimulate without converting — see Sandbox.

Convert task fields: operation = convert, input (a task name), input_format, output_format, and an optional options object with format-specific settings.

Every import must feed a convert. An import/upload, import/url or cloud-storage task that no convert task takes as input is rejected (422) — otherwise we would provision or download a file nothing would ever use. An export wired straight to an import is rejected for the same reason: only a convert produces the output an export delivers.

An export task is optional. Without one, the convert task's own result still carries a download_url to the finished file — export tasks are for delivering the result somewhere specific (a URL you fetch, a webhook, your own S3/SFTP/GCS).

Note: you may convert up to a fixed number of files per job. There is also a shortcut for a pre-created import: POST /v1/imports to get an upload form, then POST /v1/process/jobs with { "import_id": "…", "output_format": "png" }.

POST /v1/process/jobs

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1": { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
      "input_format": "jpg", "output_format": "png" },
    "export-1": { "operation": "export/url", "input": ["convert-1"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1": { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
      "input_format": "jpg", "output_format": "png" },
    "export-1": { "operation": "export/url", "input": ["convert-1"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1": { "operation": "import/upload" },
                "convert-1": { "operation": "convert", "input": "import-1",
                  "input_format": "jpg", "output_format": "png" },
                "export-1": { "operation": "export/url", "input": ["convert-1"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: JSON.stringify({
    tasks: {
      "import-1": { operation: "import/upload" },
      "convert-1": { operation: "convert", input: "import-1",
        input_format: "jpg", output_format: "png" },
      "export-1": { operation: "export/url", input: ["convert-1"] }
    }
  })
});
const job = await res.json();
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "tasks" => [
            "import-1"  => ["operation" => "import/upload"],
            "convert-1" => ["operation" => "convert", "input" => "import-1",
                            "input_format" => "jpg", "output_format" => "png"],
            "export-1"  => ["operation" => "export/url", "input" => ["convert-1"]],
        ],
    ]),
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Authorization": "Bearer {API_KEY}"},
    json={
        "tasks": {
            "import-1": {"operation": "import/upload"},
            "convert-1": {"operation": "convert", "input": "import-1",
                          "input_format": "jpg", "output_format": "png"},
            "export-1": {"operation": "export/url", "input": ["convert-1"]},
        }
    },
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = {
  tasks: {
    "import-1"  => { operation: "import/upload" },
    "convert-1" => { operation: "convert", input: "import-1",
                     input_format: "jpg", output_format: "png" },
    "export-1"  => { operation: "export/url", input: ["convert-1"] }
  }
}.to_json

job = JSON.parse(http.request(req).body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
    "tasks": {
      "import-1": { "operation": "import/upload" },
      "convert-1": { "operation": "convert", "input": "import-1",
        "input_format": "jpg", "output_format": "png" },
      "export-1": { "operation": "export/url", "input": ["convert-1"] }
    }
  }'
                                

Response

HTTP/1.1 201 Created

{
  "id": "b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6",
  "tag": "",
  "status": "waiting_upload",
  "createdAt": "2026-07-20T10:00:00+00:00",
  "tasks": [
    {
      "id": "import-1",
      "name": "import-1",
      "operation": "import/upload",
      "status": "waiting_upload",
      "result": {
        "form": {
          "url": "https://storage.googleapis.com/…signed-put-url…",
          "method": "PUT",
          "parameters": {},
          "headers": { "Content-Type": "application/octet-stream" },
          "expiresAt": "2026-07-21T10:00:00+00:00"
        }
      }
    },
    { "id": "convert-1", "name": "convert-1", "operation": "convert",
      "status": "waiting_input", "dependsOn": ["import-1"] },
    { "id": "export-1", "name": "export-1", "operation": "export/url",
      "status": "waiting_input", "dependsOn": ["convert-1"] }
  ],
  "links": { "self": "/v1/process/jobs/b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6" }
}

Upload the file

The create response returns an upload form on each import/upload task: result.form.url, result.form.method (PUT) and result.form.headers. Send the raw file bytes to that URL with those headers. The URL is a short-lived signed link; upload before it expires.

The bytes go straight from you to storage — they never pass through our servers — so an upload has no fixed size limit: the only ceiling is the quota you have left. (A file fetched by us, with import/url or from cloud storage, does have one, because we download it first.)

Send the file as a stream rather than reading it into memory. The Content-Type must be exactly the one in result.form.headers — it is part of what the URL is signed with, and anything else is rejected as a bad signature.

PUT the bytes

                                    // form = the import task's result.form from the create response
data, _ := os.ReadFile("photo.jpg")
req, _ := http.NewRequest("PUT", form.URL, bytes.NewReader(data))
req.Header.Set("Content-Type", "application/octet-stream")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
resp.Body.Close()
                                
                                    PUT /…signed-put-url… HTTP/1.1
Host: storage.googleapis.com
Content-Type: application/octet-stream

<binary file bytes>
                                
                                    // formUrl = the import task's result.form.url from the create response
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create(formUrl))
    .header("Content-Type", "application/octet-stream")
    .PUT(HttpRequest.BodyPublishers.ofFile(Path.of("photo.jpg")))
    .build();

HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
                                
                                    // form = the import task's result.form from the create response
await fetch(form.url, {
  method: form.method,     // "PUT"
  headers: form.headers,   // { "Content-Type": "application/octet-stream" }
  body: fileBlob           // a File / Blob / ArrayBuffer
});
                                
                                    <?php
// $form = the import task's result.form from the create response
$ch = curl_init($form["url"]);
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "PUT",
    CURLOPT_POSTFIELDS => file_get_contents("photo.jpg"),
    CURLOPT_HTTPHEADER => ["Content-Type: application/octet-stream"],
    CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
                                
                                    import requests

# form = the import task's result.form from the create response
with open("photo.jpg", "rb") as f:
    requests.put(form["url"], data=f, headers=form["headers"])
                                
                                    require "net/http"
require "uri"

# form = the import task's result.form from the create response
uri = URI(form["url"])
req = Net::HTTP::Put.new(uri)
req["Content-Type"] = "application/octet-stream"
req.body = File.binread("photo.jpg")

Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
                                
                                    # Use the form.url + form.headers from the import task's result
curl -X PUT "https://storage.googleapis.com/…signed-put-url…" \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @photo.jpg
                                

Import from a URL

Instead of uploading, you can let us fetch the source from a public URL. Use an import/url task with a url field, then point a convert at it — there is no upload step for that file.

Creating the job returns immediately — we do not download the source on the request. The fetch happens when the conversion runs, so a slow source never holds your request open. The URL must be publicly reachable over http or https (private, loopback and link-local addresses are rejected, and redirects are followed only to public addresses); the source may be up to 5 GB. The fetched bytes count toward your quota just like an uploaded file.

You still call complete-upload to start the conversion — that is what hands the job to a worker, which fetches the source and then converts. A single job can freely mix import/url and import/upload tasks.

Embedding bytes directly (import/base64)

For small files you can skip both upload and fetch and put the bytes in the request with an import/base64 task: set file to the base64-encoded content (a data: URI prefix is accepted and stripped). It is size-limited after decoding and counts toward your quota. Prefer import/upload or import/url for anything but small inputs.

Import from a URL

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1":  { "operation": "import/url",
                               "url": "https://example.com/photo.jpg" },
                "convert-1": { "operation": "convert", "input": "import-1",
                               "input_format": "jpg", "output_format": "png" },
                "export-1":  { "operation": "export/url", "input": ["convert-1"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: `{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json", "Authorization": "Bearer {API_KEY}"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
  "tasks": {
    "import-1":  { "operation": "import/url",
                   "url": "https://example.com/photo.jpg" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'
                                

Import base64 bytes

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1":  { "operation": "import/base64",
                               "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
                "convert-1": { "operation": "convert", "input": "import-1",
                               "input_format": "png", "output_format": "jpg" },
                "export-1":  { "operation": "export/url", "input": ["convert-1"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: `{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json", "Authorization": "Bearer {API_KEY}"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
  "tasks": {
    "import-1":  { "operation": "import/base64",
                   "file": "iVBORw0KGgoAAAANSUhEUgAA…" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'
                                

Cloud storage (S3, SFTP, Google Cloud)

Import directly from — and export directly to — your own Amazon S3 (or S3-compatible), SFTP server, or Google Cloud Storage. You pass the credentials on the task; they are used only to run that job and are never returned in any API response. The source is fetched when the conversion runs (not while your create request is open); exports run after the conversion finishes, so a failed export shows on that export task's result.

import/s3 & export/s3

FieldRequiredDescription
bucketyesBucket name.
regionAWS region (default us-east-1).
keyimport: yesObject key. For export it is optional; a key ending in / (or omitted) appends the output filename.
access_key_id, secret_access_keyyesCredentials.
session_tokenFor temporary (STS) credentials.
endpointCustom endpoint for S3-compatible stores (path-style). Must be a public host.

import/sftp & export/sftp

FieldRequiredDescription
hostyesSFTP host (must resolve to a public address).
portDefault 22.
usernameyesLogin user.
passwordone ofPassword auth.
private_keyone ofPrivate key (PEM). password is used as its passphrase if set.
pathyesRemote file (import) or destination (export). A path ending in / appends the output filename.

import/googlecloud & export/googlecloud

FieldRequiredDescription
bucketyesGCS bucket.
file (import) / filename (export)import: yesObject name. Export appends the output filename if omitted or ending in /.
client_email, private_keyyesService-account credentials (from its JSON key).
project_idProject id.

Imports are size-limited like import/url. Only public hosts are allowed for SFTP and custom S3 endpoints.

S3 → convert → SFTP

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
                          "region": "us-east-1", "key": "photos/a.png",
                          "access_key_id": "AKIA…", "secret_access_key": "…" },
                "conv": { "operation": "convert", "input": "in",
                          "input_format": "png", "output_format": "jpg" },
                "out":  { "operation": "export/sftp", "input": ["conv"],
                          "host": "sftp.example.com", "username": "me",
                          "password": "…", "path": "/uploads/" }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: `{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json", "Authorization": "Bearer {API_KEY}"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
  "tasks": {
    "in":   { "operation": "import/s3", "bucket": "my-input-bucket",
              "region": "us-east-1", "key": "photos/a.png",
              "access_key_id": "AKIA…", "secret_access_key": "…" },
    "conv": { "operation": "convert", "input": "in",
              "input_format": "png", "output_format": "jpg" },
    "out":  { "operation": "export/sftp", "input": ["conv"],
              "host": "sftp.example.com", "username": "me",
              "password": "…", "path": "/uploads/" }
  }
}'
                                

Start the conversion

POST /v1/process/jobs/{id}/complete-upload

Call this once all uploads are done. It enqueues the convert tasks and moves the job to processing. Root converts (fed by an uploaded import) start immediately; converts fed by another convert run after their upstream finishes.

POST /complete-upload

                                    req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
resp.Body.Close()
                                
                                    POST /v1/process/jobs/{JOB_ID}/complete-upload HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload"))
    .header("Authorization", "Bearer {API_KEY}")
    .POST(HttpRequest.BodyPublishers.noBody())
    .build();

HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
                                
                                    await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload", {
  method: "POST",
  headers: { "Authorization": "Bearer {API_KEY}" }
});
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
curl_exec($ch);
                                
                                    import requests

requests.post(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload",
    headers={"Authorization": "Bearer {API_KEY}"},
)
                                
                                    require "net/http"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer {API_KEY}"

Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs/{JOB_ID}/complete-upload \
  -H 'Authorization: Bearer {API_KEY}'
                                

Get a job

GET /v1/process/jobs/{id}

Returns the job with its tasks and their results. Poll this until the job reaches a terminal state. The export task's result.url is the download link once finished.

Status lifecycle

StatusMeaning
waiting_uploadJob created; waiting for your upload(s).
processingConversions are running.
finishedAll conversions succeeded.
failedAt least one conversion failed (per-task results still show which).

GET /v1/process/jobs/{id}

                                    req, _ := http.NewRequest("GET", "https://api.convert.online/v1/process/jobs/{JOB_ID}", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, err := http.DefaultClient.Do(req)
if err != nil {
	panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
                                
                                    GET /v1/process/jobs/{JOB_ID} HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}"))
    .header("Authorization", "Bearer {API_KEY}")
    .GET()
    .build();

HttpResponse<String> res = HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofString());
System.out.println(res.body());
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}", {
  headers: { "Authorization": "Bearer {API_KEY}" }
});
const job = await res.json();
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

job = requests.get(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}",
    headers={"Authorization": "Bearer {API_KEY}"},
).json()
                                
                                    require "net/http"
require "json"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer {API_KEY}"

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl https://api.convert.online/v1/process/jobs/{JOB_ID} \
  -H 'Authorization: Bearer {API_KEY}'
                                

Finished job

HTTP/1.1 200 OK

{
  "id": "b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6",
  "status": "finished",
  "createdAt": "2026-07-20T10:00:00+00:00",
  "endedAt": "2026-07-20T10:00:07+00:00",
  "tasks": [
    { "name": "convert-1", "operation": "convert", "status": "finished" },
    {
      "name": "export-1", "operation": "export/url", "status": "finished",
      "result": {
        "url": "https://api.convert.online/v1/download/eyJ…token…",
        "filename": "photo.png"
      }
    }
  ],
  "links": { "self": "/v1/process/jobs/b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6" }
}

Job events (SSE)

GET /v1/process/jobs/{id}/events

Opens a Server-Sent Events stream that pushes the job's status as it changes, ending when the job is finished/failed. Useful to avoid polling.

The stream stays open for the whole conversion, so it must be served without proxy buffering. If your client or network can't hold a long connection, polling (Get a job) is the simpler and equally reliable option.

Listen for events

                                    package main

import (
	"bufio"
	"fmt"
	"net/http"
)

func main() {
	req, _ := http.NewRequest("GET", "https://api.convert.online/v1/process/jobs/{JOB_ID}/events", nil)
	req.Header.Set("Authorization", "Bearer {API_KEY}")
	req.Header.Set("Accept", "text/event-stream")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		fmt.Println(scanner.Text())
	}
}
                                
                                    GET /v1/process/jobs/{JOB_ID}/events HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
Accept: text/event-stream
                                
                                    import java.net.URI;
import java.net.http.*;

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/process/jobs/{JOB_ID}/events"))
    .header("Authorization", "Bearer {API_KEY}")
    .header("Accept", "text/event-stream")
    .build();

HttpClient.newHttpClient()
    .send(req, HttpResponse.BodyHandlers.ofLines())
    .body().forEach(System.out::println);
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs/{JOB_ID}/events", {
  headers: { "Authorization": "Bearer {API_KEY}", "Accept": "text/event-stream" }
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log(decoder.decode(value));
}
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/process/jobs/{JOB_ID}/events");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}", "Accept: text/event-stream"],
    CURLOPT_WRITEFUNCTION => function ($ch, $chunk) {
        echo $chunk;            // each event as it arrives
        return strlen($chunk);
    },
]);
curl_exec($ch);
                                
                                    import requests

with requests.get(
    "https://api.convert.online/v1/process/jobs/{JOB_ID}/events",
    headers={"Authorization": "Bearer {API_KEY}", "Accept": "text/event-stream"},
    stream=True,
) as r:
    for line in r.iter_lines():
        if line:
            print(line.decode())
                                
                                    require "net/http"
require "uri"

uri = URI("https://api.convert.online/v1/process/jobs/{JOB_ID}/events")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  req = Net::HTTP::Get.new(uri)
  req["Authorization"] = "Bearer {API_KEY}"
  req["Accept"] = "text/event-stream"
  http.request(req) { |res| res.read_body { |chunk| print chunk } }
end
                                
                                    curl -N https://api.convert.online/v1/process/jobs/{JOB_ID}/events \
  -H 'Authorization: Bearer {API_KEY}'
                                

Download the result

GET /v1/download/{token}

A finished export/url task returns a ready-to-use download link in result.url — that link is this endpoint. Fetching it meters the download against your quota (the first download of each file is free) and then 302-redirects to a short-lived storage URL, so follow redirects. Re-downloads after the free one consume quota; once exhausted it returns 402.

Download

                                    req, _ := http.NewRequest("GET", "https://api.convert.online/v1/download/eyJ…token…", nil)
req.Header.Set("Authorization", "Bearer {API_KEY}")

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

out, _ := os.Create("photo.png")
defer out.Close()
io.Copy(out, resp.Body)
                                
                                    GET /v1/download/eyJ…token… HTTP/1.1
Host: api.convert.online
Authorization: Bearer {API_KEY}
                                
                                    HttpClient client = HttpClient.newBuilder()
    .followRedirects(HttpClient.Redirect.NORMAL)
    .build();

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.convert.online/v1/download/eyJ…token…"))
    .header("Authorization", "Bearer {API_KEY}")
    .build();

client.send(req, HttpResponse.BodyHandlers.ofFile(Path.of("photo.png")));
                                
                                    const res = await fetch("https://api.convert.online/v1/download/eyJ…token…", {
  headers: { "Authorization": "Bearer {API_KEY}" }
});
const blob = await res.blob(); // save/stream as needed
                                
                                    <?php
$ch = curl_init("https://api.convert.online/v1/download/eyJ…token…");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer {API_KEY}"],
]);
file_put_contents("photo.png", curl_exec($ch));
                                
                                    import requests

# The export result.url is a ready-to-use link (redirects to storage).
r = requests.get("https://api.convert.online/v1/download/eyJ…token…",
                 headers={"Authorization": "Bearer {API_KEY}"}, allow_redirects=True)
with open("photo.png", "wb") as f:
    f.write(r.content)
                                
                                    require "open-uri"

URI.open("https://api.convert.online/v1/download/eyJ…token…",
         "Authorization" => "Bearer {API_KEY}") do |src|
  File.binwrite("photo.png", src.read)
end
                                
                                    # The export result.url is already a full, ready-to-use link:
curl -L "https://api.convert.online/v1/download/eyJ…token…" -o photo.png
                                

Multi-file jobs

One job can carry several files: add multiple import/upload + convert + export/url task sets. Each import gets its own upload form; upload them all, then call complete-upload once. Every convert runs, and the job is finished only when all converts succeed (it is failed if any fails, with per-task results preserved). There is a limit on the number of convert tasks per job.

Chained conversions

A convert task's input can be another convert, so a file flows through several conversions in one job (for example PNG → PDF → JPG). Set the downstream convert's input to the upstream convert's name and its input_format to the upstream's output_format.

The graph must be acyclic and trace back to an import. Each step is a real conversion, so each step meters quota.

Chained job (png → pdf → jpg)

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1":  { "operation": "import/upload" },
                "convert-1": { "operation": "convert", "input": "import-1",
                               "input_format": "png", "output_format": "pdf" },
                "convert-2": { "operation": "convert", "input": "convert-1",
                               "input_format": "pdf", "output_format": "jpg" },
                "export-1":  { "operation": "export/url", "input": ["convert-2"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: `{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json", "Authorization": "Bearer {API_KEY}"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "png", "output_format": "pdf" },
    "convert-2": { "operation": "convert", "input": "convert-1",
                   "input_format": "pdf", "output_format": "jpg" },
    "export-1":  { "operation": "export/url", "input": ["convert-2"] }
  }
}'
                                

Webhooks

Get notified when a job finishes instead of polling. Add an export/webhook task with a url (or set a job-level webhook_url). When the job reaches a terminal state, we POST a JSON event to that URL.

Each delivery carries these headers:

HeaderValue
X-Convert-Eventjob.finished or job.failed
X-Convert-Signaturesha256=<HMAC-SHA256 of the raw body>
X-Convert-DeliveryUnique delivery id (use it to dedupe).
X-Convert-AttemptDelivery attempt number.

Delivery is retried with exponential backoff (up to 5 attempts: immediately, then +30s, +2m, +10m, +30m). Respond 2xx to acknowledge. Only public http(s) targets are allowed.

Verifying the signature

Every delivery is signed so you can confirm it genuinely came from us. Take your webhook signing secret from Dashboard → API Keys, compute HMAC-SHA256(raw_request_body) with it, and compare — using a constant-time comparison — against the hex value in the X-Convert-Signature header (the part after sha256=). Sign the raw body bytes, before any JSON parsing. Reject the request if it doesn't match. You can rotate the secret from the dashboard at any time.

Add an export/webhook task

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer {API_KEY}")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json
Authorization: Bearer {API_KEY}

{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "tasks": {
                "import-1":  { "operation": "import/upload" },
                "convert-1": { "operation": "convert", "input": "import-1",
                               "input_format": "jpg", "output_format": "png" },
                "export-1":  { "operation": "export/url", "input": ["convert-1"] },
                "notify":    { "operation": "export/webhook",
                               "url": "https://your-app.example.com/hooks/convert" }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer {API_KEY}")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer {API_KEY}"
  },
  body: `{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {API_KEY}",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json", "Authorization": "Bearer {API_KEY}"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req["Authorization"] = "Bearer {API_KEY}"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer {API_KEY}' \
  -d '{
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] },
    "notify":    { "operation": "export/webhook",
                   "url": "https://your-app.example.com/hooks/convert" }
  }
}'
                                

Delivered event

POST /hooks/convert  (from Convert.Online)
X-Convert-Event: job.finished
X-Convert-Signature: sha256=9f86d08…
X-Convert-Delivery: 8f3b2c1a9d0e4f5a
X-Convert-Attempt: 1

{
  "event": "job.finished",
  "id": "b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6",
  "status": "finished",
  "tasks": {
    "export-1": { "operation": "export/url", "status": "finished",
      "result": { "url": "https://api.convert.online/v1/download/eyJ…", "filename": "photo.png" } }
  }
}

Verify the signature

                                    import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
)

// secret = your webhook signing secret from the dashboard
func verify(rawBody []byte, signatureHeader, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
                                
                                    import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;

// secret = your webhook signing secret from the dashboard
boolean verify(byte[] rawBody, String signatureHeader, String secret) throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
    byte[] h = mac.doFinal(rawBody);
    StringBuilder sb = new StringBuilder("sha256=");
    for (byte b : h) sb.append(String.format("%02x", b));
    return MessageDigest.isEqual(sb.toString().getBytes(), signatureHeader.getBytes());
}
                                
                                    const crypto = require("crypto");

// secret = your webhook signing secret from the dashboard
function verify(rawBody, signatureHeader, secret) {
  const expected = "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: use express.raw({type: "*/*"}) so req.body is the exact bytes
app.post("/hooks/convert", (req, res) => {
  if (!verify(req.body, req.get("X-Convert-Signature"), process.env.WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body.toString());
  // … handle event.status …
  res.sendStatus(200);
});
                                
                                    <?php
// $secret = your webhook signing secret from the dashboard
$raw = file_get_contents("php://input");
$sig = $_SERVER["HTTP_X_CONVERT_SIGNATURE"] ?? "";
$expected = "sha256=" . hash_hmac("sha256", $raw, $secret);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// … handle $event["status"] …
http_response_code(200);
                                
                                    import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

# Flask
@app.post("/hooks/convert")
def convert_hook():
    if not verify(request.get_data(), request.headers.get("X-Convert-Signature"), WEBHOOK_SECRET):
        abort(401)
    event = request.get_json()
    # … handle event["status"] …
    return "", 200
                                
                                    require "openssl"
require "rack/utils"

# secret = your webhook signing secret from the dashboard
def verify(raw_body, signature_header, secret)
  expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)
  Rack::Utils.secure_compare(expected, signature_header.to_s)
end
                                

Sandbox

Add "sandbox": true to the create body to simulate a job: it is validated and a realistic finished response is returned with placeholder download URLs — but no conversion runs, no file is uploaded, no API key is required, and no quota is used. Use it to develop and test your integration and to see the exact response shape.

Sandbox request

                                    package main

import (
	"bytes"
	"fmt"
	"net/http"
)

func main() {
	payload := []byte(`{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`)

	req, _ := http.NewRequest("POST", "https://api.convert.online/v1/process/jobs", bytes.NewBuffer(payload))
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println("Status:", resp.Status)
}
                                
                                    POST /v1/process/jobs HTTP/1.1
Host: api.convert.online
Content-Type: application/json

{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
                                
                                    import java.net.URI;
import java.net.http.*;

public class CreateJob {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "sandbox": true,
              "tasks": {
                "import-1":  { "operation": "import/upload" },
                "convert-1": { "operation": "convert", "input": "import-1",
                               "input_format": "jpg", "output_format": "png" },
                "export-1":  { "operation": "export/url", "input": ["convert-1"] }
              }
            }""";

        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://api.convert.online/v1/process/jobs"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

        HttpResponse<String> res = HttpClient.newHttpClient()
            .send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(res.body());
    }
}
                                
                                    const res = await fetch("https://api.convert.online/v1/process/jobs", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: `{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}`
});
const job = await res.json();
                                
                                    <?php
$body = <<<'JSON'
{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON;

$ch = curl_init("https://api.convert.online/v1/process/jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
    ],
    CURLOPT_POSTFIELDS => $body,
]);
$job = json_decode(curl_exec($ch), true);
                                
                                    import requests

body = '''{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'''

res = requests.post(
    "https://api.convert.online/v1/process/jobs",
    headers={"Content-Type": "application/json"},
    data=body,
)
job = res.json()
                                
                                    require "net/http"
require "json"
require "uri"

body = <<~JSON
{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}
JSON

uri = URI("https://api.convert.online/v1/process/jobs")
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = body

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
job = JSON.parse(res.body)
                                
                                    curl -X POST https://api.convert.online/v1/process/jobs \
  -H 'Content-Type: application/json' \
  -d '{
  "sandbox": true,
  "tasks": {
    "import-1":  { "operation": "import/upload" },
    "convert-1": { "operation": "convert", "input": "import-1",
                   "input_format": "jpg", "output_format": "png" },
    "export-1":  { "operation": "export/url", "input": ["convert-1"] }
  }
}'
                                

Simulated response

HTTP/1.1 200 OK

{
  "id": "sandbox-4f8a2c1b9d0e3a6c",
  "status": "finished",
  "sandbox": true,
  "tasks": [
    { "name": "export-1", "operation": "export/url", "status": "finished",
      "result": { "url": "https://api.convert.online/v1/download/SANDBOX-SAMPLE", "filename": "sample-output" } }
  ],
  "note": "Sandbox simulation — no conversion ran and no quota was used."
}

Errors

Errors are returned as JSON with an HTTP status code:

{ "status": "error", "message": "…" }

CodeMeaning
401Missing or invalid API key.
402Insufficient quota for this file / download.
403Key not allowed from this IP.
404Job, endpoint or download token not found.
409Job is not waiting for upload, or an input file wasn't uploaded.
422Invalid job (bad task graph, unsupported operation, cycle, over the task limit, or more conversions than your plan runs at once).
429Rate limit exceeded (see retry_after).
503Conversion queue is full or temporarily unavailable — retry shortly.

Tools

API Job Builder

The visual Job Builder lets you assemble a job by wiring import/convert/export nodes, set per-format options, and see the request payload and ready-to-run code in eight languages update live. You can also run the job (Live) or simulate it (Sandbox) straight from the page.

MCP Server

The Model Context Protocol server at https://mcp.convert.online exposes conversions to AI assistants (ChatGPT, Claude and other MCP clients) over Streamable HTTP. Tools: list_formats, convert (from a public URL) and get_job. Authenticate with your API key.