Documentation

Everything you need to convert documents with the FileChanger API

Authentication

All API requests require an API key passed in the X-Api-Key header. Create your key from the dashboard.

Request Headers

HeaderRequiredDescription
X-Api-KeyYesYour API key from the dashboard (e.g. fc_abc123...)
  • API keys are prefixed with fc_ and must be kept secret.
  • Keys are hashed before storage. If you lose your key, reset it from the dashboard.
  • One key per account. Resetting your key invalidates the previous one immediately.

Error Responses

StatusErrorDescription
401Missing API keyThe X-Api-Key header was not provided
401Invalid API keyThe API key was not found or is malformed
403API key disabledYour API key has been disabled
403Quota exhaustedConversion quota exhausted, upgrade or wait for reset
429Rate limit exceededToo many requests in the last minute, slow down and retry

Code Examples

# Pass your API key in the X-Api-Key header
curl https://filechanger.io/api/formats \
  -H "X-Api-Key: fc_your_api_key_here"
import requests

headers = {"X-Api-Key": "fc_your_api_key_here"}

response = requests.get(
    "https://filechanger.io/api/formats",
    headers=headers,
)

print(response.json())
const response = await fetch("https://filechanger.io/api/formats", {
  headers: {
    "X-Api-Key": "fc_your_api_key_here",
  },
});

const data = await response.json();
console.log(data);
HttpClient client = HttpClient.newHttpClient();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://filechanger.io/api/formats"))
    .header("X-Api-Key", "fc_your_api_key_here")
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
import Network.HTTP.Simple

main :: IO ()
main = do
  let request = setRequestHeader "X-Api-Key" ["fc_your_api_key_here"]
              $ "GET https://filechanger.io/api/formats"

  response <- httpBS request
  print (getResponseBody response)

Create a Conversion Job

Upload a file and the source/target formats to start a conversion job. Conversion is asynchronous: this returns a job id immediately, then you poll the job and download its output. The same flow handles every format, text or binary, of any size.

Request Headers

HeaderRequiredDescription
X-Api-KeyYesYour API key
Content-TypeYesMust be multipart/form-data
  • Endpoint: POST /api/jobs
  • Form fields: "file" (the uploaded file), "from" (source format), "to" (target format)
  • Response: JSON with "job_id" (string) and "status" ("pending")
  • Next step: poll GET /api/jobs/{job_id} until the status is "done", then download the output

Error Responses

StatusErrorDescription
400Unsupported input formatThe source format is not supported for reading
400Unsupported output formatThe target format is not supported for writing
400Missing form fieldThe multipart upload must include the 'file', 'from', and 'to' fields
400File too largeThe uploaded file exceeds the maximum size limit
403Quota exhaustedConversion quota exhausted, upgrade or wait for reset
429Rate limit exceededToo many requests in the last minute, slow down and retry

Code Examples

# Upload a Markdown file to convert to DOCX, capturing the job id
curl -X POST "https://filechanger.io/api/jobs" \
  -H "X-Api-Key: fc_your_api_key_here" \
  -F "file=@document.md" \
  -F "from=markdown" \
  -F "to=docx"

# Response:
# {"job_id": "42", "status": "pending"}
import requests

with open("document.md", "rb") as f:
    response = requests.post(
        "https://filechanger.io/api/jobs",
        headers={"X-Api-Key": "fc_your_api_key_here"},
        files={"file": f},
        data={"from": "markdown", "to": "docx"},
    )

job = response.json()
print("Job id:", job["job_id"])
const formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("from", "markdown");
formData.append("to", "docx");

const response = await fetch("https://filechanger.io/api/jobs", {
  method: "POST",
  headers: { "X-Api-Key": "fc_your_api_key_here" },
  body: formData,
});

const job = await response.json();
console.log("Job id:", job.job_id);
import java.net.http.HttpRequest.BodyPublishers;
import java.nio.file.Path;

String boundary = "----FormBoundary" + System.currentTimeMillis();
byte[] fileBytes = java.nio.file.Files.readAllBytes(Path.of("document.md"));

String body = "--" + boundary + "\r\n"
    + "Content-Disposition: form-data; name=\"file\"; filename=\"document.md\"\r\n"
    + "Content-Type: text/markdown\r\n\r\n"
    + new String(fileBytes) + "\r\n"
    + "--" + boundary + "\r\nContent-Disposition: form-data; name=\"from\"\r\n\r\nmarkdown\r\n"
    + "--" + boundary + "\r\nContent-Disposition: form-data; name=\"to\"\r\n\r\ndocx\r\n"
    + "--" + boundary + "--";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://filechanger.io/api/jobs"))
    .header("X-Api-Key", "fc_your_api_key_here")
    .header("Content-Type", "multipart/form-data; boundary=" + boundary)
    .POST(BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
import Network.HTTP.Client
import Network.HTTP.Client.MultipartFormData

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings
  initialReq <- parseRequest "POST https://filechanger.io/api/jobs"
  let req = initialReq
        { requestHeaders = [("X-Api-Key", "fc_your_api_key_here")] }
  request <- formDataBody
    [ partFileSource "file" "document.md"
    , partBS "from" "markdown"
    , partBS "to" "docx"
    ] req

  response <- httpLbs request manager
  print (responseBody response)

Poll and Download

After creating a job, poll its status until it is done, then download the converted output. The output is streamed back with the correct filename and content type, and is deleted from storage right after delivery.

Request Headers

HeaderRequiredDescription
X-Api-KeyYesYour API key
  • Status endpoint: GET /api/jobs/{job_id}
  • Status response: JSON with "status" (pending, running, done, or failed), "from", "to", and (once done) "output_size"; "error" appears on failure
  • Download endpoint: GET /api/jobs/{job_id}/output
  • Download timing: returns 409 until the job is done, then streams the output once (the stored objects are deleted after delivery)

Response Headers

HeaderDescription
Content-DispositionAttachment filename with the correct extension for the output format (on the output endpoint)
Content-TypeMIME type of the converted output (on the output endpoint)

Error Responses

StatusErrorDescription
404Not foundNo job with that id belongs to your account
409Not readyThe job has not finished yet; keep polling the status endpoint
500Conversion failedThe job's status is "failed"; the error field carries a generic message

Code Examples

# Poll the job status until it is done
curl "https://filechanger.io/api/jobs/42" \
  -H "X-Api-Key: fc_your_api_key_here"

# Response while running:  {"status": "running", "from": "markdown", "to": "docx"}
# Response when finished:  {"status": "done", "from": "markdown", "to": "docx", "output_size": 1234}

# Download the converted output once the status is "done"
curl "https://filechanger.io/api/jobs/42/output" \
  -H "X-Api-Key: fc_your_api_key_here" \
  -o document.docx
import time
import requests

headers = {"X-Api-Key": "fc_your_api_key_here"}
job_id = "42"

while True:
    status = requests.get(
        f"https://filechanger.io/api/jobs/{job_id}", headers=headers
    ).json()
    if status["status"] == "done":
        break
    if status["status"] == "failed":
        raise RuntimeError(status.get("error", "conversion failed"))
    time.sleep(0.5)

output = requests.get(
    f"https://filechanger.io/api/jobs/{job_id}/output", headers=headers
)
with open("document.docx", "wb") as out:
    out.write(output.content)
const headers = { "X-Api-Key": "fc_your_api_key_here" };
const jobId = "42";

let status;
do {
  await new Promise((r) => setTimeout(r, 500));
  const res = await fetch("https://filechanger.io/api/jobs/" + jobId, { headers });
  status = await res.json();
  if (status.status === "failed") throw new Error(status.error);
} while (status.status !== "done");

const output = await fetch(
  "https://filechanger.io/api/jobs/" + jobId + "/output", { headers }
);
const blob = await output.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.docx";
a.click();
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Path;

String jobId = "42";
String statusUrl = "https://filechanger.io/api/jobs/" + jobId;

String status;
do {
    Thread.sleep(500);
    HttpRequest poll = HttpRequest.newBuilder()
        .uri(URI.create(statusUrl))
        .header("X-Api-Key", "fc_your_api_key_here")
        .GET().build();
    status = client.send(poll, BodyHandlers.ofString()).body();
} while (!status.contains("\"status\":\"done\""));

HttpRequest download = HttpRequest.newBuilder()
    .uri(URI.create(statusUrl + "/output"))
    .header("X-Api-Key", "fc_your_api_key_here")
    .GET().build();

HttpResponse<byte[]> response = client.send(
    download, BodyHandlers.ofByteArray());
java.nio.file.Files.write(Path.of("document.docx"), response.body());
import Network.HTTP.Simple
import qualified Data.ByteString.Lazy as BL
import Control.Concurrent (threadDelay)
import qualified Data.ByteString.Char8 as B8

main :: IO ()
main = do
  let jobId = "42"
      key = setRequestHeader "X-Api-Key" ["fc_your_api_key_here"]
      pollUntilDone = do
        resp <- httpBS (key $ "GET https://filechanger.io/api/jobs/" <> jobId)
        if "\"status\":\"done\"" `B8.isInfixOf` getResponseBody resp
          then pure ()
          else threadDelay 500000 >> pollUntilDone
  pollUntilDone
  output <- httpLBS (key $ "GET https://filechanger.io/api/jobs/" <> jobId <> "/output")
  BL.writeFile "document.docx" (getResponseBody output)

Supported Formats

Query the API for the full list of supported input and output formats. Not all formats can be used in both directions. This endpoint is public and needs no API key.

  • Endpoint: GET /api/formats
  • Text formats: markdown, html, latex, rst, org, textile, mediawiki, dokuwiki, commonmark, gfm, docbook, jira, typst, djot, asciidoc, plain, native, json
  • Binary formats: docx, epub, odt, rtf (the same POST /api/jobs flow handles these)
  • Audio formats: mp3, wav, flac, ogg, opus (transcode between audio formats)
  • Video formats: mp4, mov, avi, mkv, webm, flv, wmv, mpeg, m4v in; mp4, mkv, webm, av1 out
  • Format aliases: md (markdown), html5 (html), tex (latex)

Code Examples

curl "https://filechanger.io/api/formats"

# Response:
# {
#   "input_formats": ["markdown", "html", "latex", ...],
#   "output_formats": ["markdown", "html", "latex", ...]
# }
import requests

response = requests.get("https://filechanger.io/api/formats")

formats = response.json()
print("Input formats:", formats["input_formats"])
print("Output formats:", formats["output_formats"])
const response = await fetch("https://filechanger.io/api/formats");

const formats = await response.json();
console.log("Input formats:", formats.input_formats);
console.log("Output formats:", formats.output_formats);
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://filechanger.io/api/formats"))
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

System.out.println(response.body());
import Network.HTTP.Simple

main :: IO ()
main = do
  let request = "GET https://filechanger.io/api/formats"

  response <- httpBS request
  print (getResponseBody response)

n8n Integration

Convert files inside your n8n workflows with the FileChanger community node. The node takes a binary input, converts it, and hands the converted file to the next node as binary output.

  • Install: in n8n go to Settings > Community Nodes > Install and enter the package name n8n-nodes-filechanger
  • Credentials: create a FileChanger API credential with your API key from the dashboard. The Base URL defaults to https://filechanger.io; only change it for a self-hosted instance. The Test button calls GET /api/formats and expects a 200
  • Operations: Convert a File (send a binary, get the converted binary back) and List Formats (fetch the supported input and output formats)
  • Formats: the From Format and To Format dropdowns are loaded live from /api/formats, so they always match what the API supports

Zapier Integration

Convert files between formats inside your Zaps with the FileChanger Zapier app.

  • Connect: add a FileChanger action to a Zap and connect your account with your API key from the dashboard. The Base URL defaults to https://filechanger.io; only change it for a self-hosted instance
  • Actions: Convert File (converts a file from one format to another) and List Formats (returns the supported input and output formats)
  • Formats: the From Format and To Format dropdowns are populated dynamically from the API
  • Verification: Zapier verifies the connection with a GET /api/formats call that must return a 200

Make Integration

Use FileChanger from your Make (formerly Integromat) scenarios with the FileChanger custom app.

  • Install: add the FileChanger app to your Make organisation via its install link, then create a connection
  • Connection: two fields, Base URL (defaults to https://filechanger.io) and API Key (from the dashboard). Make validates the connection with a GET /api/formats call that must return a 200
  • Modules: Convert a File (upload a file with from/to format identifiers, receive the converted file) and List Formats (fetch the supported input and output formats)
  • Formats: Make does not offer dropdowns, so From Format and To Format are free-text fields. Run the List Formats module first to discover the identifiers (e.g. markdown, docx, mp3)
  • Output: File Data is the converted binary, ready to feed into an upload module; File Name is taken from the response's Content-Disposition header

Ready to get started?

Create Your API Key