Blog

Convert documents to Markdown from any language

24 Aug 2026 · 6 min read


AnyMD's document to Markdown API works from any language that speaks HTTP. Python, Node.js, Rust, Go, Java, Ruby, PHP, C#, or curl from the terminal — the same endpoint, the same authentication, the same clean Markdown output. No SDK to install, no language-specific binding to maintain, no client library going out of date.

This post walks through the universal approach: one REST API, one authentication header, 15+ input formats, and every major programming language ready to use it in under 60 seconds.

The API contract

Every conversion hits the same endpoint:

POST https://anymd.net/api/convert
Authorization: Bearer <your-api-key>
Content-Type: multipart/form-data
Body: file=@document.pdf

Response: GitHub-Flavored Markdown as the plain-text body. HTTP 200 means success, 4xx/5xx means failure with a JSON error message. That's it. One contract, 14 languages, zero ceremony.

curl — the universal client

If you can open a terminal, you can convert documents:

curl -X POST https://anymd.net/api/convert \
  -H "Authorization: Bearer ***" \
  -F "file=@report.pdf" \
  -o report.md

That's a production-grade conversion in a single line. No package manager, no runtime, no import. The shell handles streaming, error codes, and file I/O natively.

Python — httpx (async)

Python developers get the richest SDK ecosystem. Here's the async version with httpx:

import httpx


async def convert_doc(file_path: str, api_key: str) -> str:
    async with httpx.AsyncClient() as client:
        with open(file_path, "rb") as f:
            resp = await client.post(
                "https://anymd.net/api/convert",
                headers={"Authorization": f"Bearer {api_key}"},
                files={"file": f},
            )
        resp.raise_for_status()
        return resp.text


# Usage
markdown = await convert_doc("paper.pdf", "sk-...")
print(markdown[:500])

For synchronous code, requests works the same way — just swap async with for with and remove await.

LibraryLinesAsync?Install
httpx12Yespip install httpx
requests10Nopip install requests
aiohttp14Yespip install aiohttp

See the dedicated Python tutorial for batch processing, pagination, and error handling patterns.

Node.js — fetch (native)

Node.js 18+ ships with fetch built in. No dependencies needed:

import { readFileSync } from "node:fs";
import { Blob } from "node:buffer";

async function convertDoc(filePath, apiKey) {
  const file = readFileSync(filePath);
  const form = new FormData();
  form.set("file", new Blob([file]));

  const resp = await fetch("https://anymd.net/api/convert", {
    method: "POST",
    headers: { Authorization: `Bearer ${apiKey}` },
    body: form,
  });

  if (!resp.ok) {
    const err = await resp.json().catch(() => ({}));
    throw new Error(err.error || `HTTP ${resp.status}`);
  }

  return await resp.text();
}

// Usage
const md = await convertDoc("deck.pptx", "sk-...");
console.log(md);

TypeScript users get autocompletion on the response — the API returns text/plain; charset=utf-8 with Markdown content. The full Node.js guide covers Express middleware, streaming, and rate limiting.

Rust — reqwest

Rust clients paired with AnyMD's Rust server achieve the lowest end-to-end latency. The tokio runtime makes concurrent batch conversions seamless:

use reqwest::Client;
use std::path::Path;
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};

async fn convert_doc(path: &Path, api_key: &str) -> anyhow::Result<String> {
    let file = File::open(path).await?;
    let stream = FramedRead::new(file, BytesCodec::new());
    let body = reqwest::Body::wrap_stream(stream);

    let client = Client::new();
    let resp = client
        .post("https://anymd.net/api/convert")
        .header("Authorization", format!("Bearer {api_key}"))
        .multipart(
            reqwest::multipart::Part::stream(body)
                .file_name(path.to_string_lossy().to_string()),
        )
        .send()
        .await?;

    let status = resp.status();
    let text = resp.text().await?;

    if !status.is_success() {
        anyhow::bail!("HTTP {status}: {text}");
    }

    Ok(text)
}

The Rust deep-dive includes reqwest connection pooling benchmarks and server-side streaming patterns.

Go — net/http

Go's standard library handles everything. A clean, context-aware client:

package main

import (
    "bytes"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
)

func convertDoc(filePath, apiKey string) (string, error) {
    file, err := os.Open(filePath)
    if err != nil {
        return "", err
    }
    defer file.Close()

    var buf bytes.Buffer
    w := multipart.NewWriter(&buf)
    part, _ := w.CreateFormFile("file", filePath)
    io.Copy(part, file)
    w.Close()

    req, _ := http.NewRequest("POST",
        "https://anymd.net/api/convert", &buf)
    req.Header.Set("Authorization", "Bearer "+apiKey)
    req.Header.Set("Content-Type", w.FormDataContentType())

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

    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK {
        return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, body)
    }

    return string(body), nil
}

The Go tutorial adds retry logic, connection pooling, and shared client patterns for production use.

Java — HttpClient (JDK 11+)

Modern Java needs no external dependencies. The built-in HttpClient handles multipart:

import java.net.URI;
import java.net.http.*;
import java.nio.file.Files;
import java.nio.file.Path;

public class AnyMDClient {
    static String convertDoc(String filePath, String apiKey) throws Exception {
        var boundary = "---boundary---";
        var fileBytes = Files.readAllBytes(Path.of(filePath));
        var body = HttpRequest.BodyPublishers.ofByteArray(
            ("--" + boundary + "\r\n" +
             "Content-Disposition: form-data; name=\"file\"; filename=\"" +
             Path.of(filePath).getFileName() + "\"\r\n" +
             "Content-Type: application/octet-stream\r\n\r\n").getBytes()
        );
        body = HttpRequest.BodyPublishers.concat(
            body,
            HttpRequest.BodyPublishers.ofByteArray(fileBytes),
            HttpRequest.BodyPublishers.ofString("\r\n--" + boundary + "--\r\n")
        );

        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
            .uri(URI.create("https://anymd.net/api/convert"))
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .POST(body)
            .build();

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new RuntimeException("HTTP " + response.statusCode() + ": " + response.body());
        }
        return response.body();
    }
}

Ruby — Net::HTTP

Ruby's standard library with the net-http and uri gems (both built-in):

require "net/http"
require "uri"

def convert_doc(file_path, api_key)
  uri = URI("https://anymd.net/api/convert")
  file = File.open(file_path)

  request = Net::HTTP::Post::Multipart.new(
    uri,
    { "file" => UploadIO.new(file, "application/octet-stream", File.basename(file_path)) },
    { "Authorization" => "Bearer #{api_key}" }
  )

  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
    http.request(request)
  end

  raise "HTTP #{response.code}: #{response.body}" unless response.code.to_i == 200
  response.body
ensure
  file&.close
end

PHP — cURL

PHP's cURL wrapper remains one of the most concise multipart clients:

function convertDoc(string $filePath, string $apiKey): string {
    $ch = curl_init("https://anymd.net/api/convert");
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"],
        CURLOPT_POSTFIELDS => [
            "file" => new CURLFile($filePath),
        ],
        CURLOPT_RETURNTRANSFER => true,
    ]);

    $result = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: $result");
    }
    return $result;
}

C# — HttpClient

Modern .NET with System.Net.Http:

using System.Net.Http.Headers;

async Task<string> ConvertDoc(string filePath, string apiKey)
{
    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue("Bearer", apiKey);

    using var form = new MultipartFormDataContent();
    using var fileStream = File.OpenRead(filePath);
    form.Add(new StreamContent(fileStream), "file", Path.GetFileName(filePath));

    var response = await client.PostAsync(
        "https://anymd.net/api/convert", form);

    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync();
}

Format support — one API, 15+ formats

Every language client above works with every format. The same file=<document> multipart field, the same authentication, the same Markdown output — regardless of whether you're sending a PDF, a DOCX from 2007, a LibreOffice ODT, or an EPUB ebook:

FormatExtensionTypical time (100 KB)
PDF.pdf350 ms
DOCX.docx280 ms
PPTX.pptx420 ms
XLSX.xlsx520 ms
ODT.odt260 ms
EPUB.epub310 ms
RTF.rtf220 ms
HTML.html180 ms
Markdown.md50 ms (passthrough)
CSV.csv150 ms
XML.xml190 ms
JSON.json170 ms

Times are median over 1,000 documents at the 100 KB size level via AnyMD's production API. Your mileage depends on file complexity and page count.

What you don't need

  • No SDK install — no pip install, npm install, cargo add, or go get required. The HTTP POST is the SDK.
  • No version management — the API contract is stable. You don't track client library versions or breaking changes.
  • No format-specific libraries — one endpoint handles PDF, DOCX, PPTX, XLSX, EPUB, ODT, RTF, HTML, CSV, XML, JSON, and more. No pdfplumber for PDFs, python-docx for DOCX, openpyxl for spreadsheets.
  • No local dependencies — the conversion engine runs on the server. No PyMuPDF install, no LibreOffice headless, no Pandoc binary to keep updated.

For in-depth language-specific patterns, see our dedicated tutorials: Python, Node.js, Rust, Go, and Bash + jq. For pricing and rate limits, see the plans page.

Conclusion

AnyMD's REST API is the universal document conversion layer. It works the same way from every language, every framework, every platform. One POST request, one authentication key, 15+ formats — clean Markdown out.

Get your free API key — 100 pages/month free, no credit card required. Then convert any document from any language in under 60 seconds.


← Read more →