HTML → PDF conversion API

Turn your HTML pages into a pixel-perfect PDF

Send a URL or raw HTML and get back a PDF. Leave managing headless browseres, babysitting queues, scaling and concurrency to us.

Free tier · 100 documents / month · no credit card · unlimited watermarked test mode
curl -X POST https://api.transformy.io/v1/pdf/chrome \
  -H "Authorization: Bearer tfy_test_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com" }' \
  -o example.pdf
import { Transformy } from "transformy";

const client = new Transformy(process.env.TRANSFORMY_KEY);

const { file } = await client.pdf.chrome({
  url: "https://example.com",
  output: "json",
});

console.log(file.url);
from transformy import Transformy

client = Transformy(os.environ["TRANSFORMY_KEY"])

result = client.pdf.chrome(
    url="https://example.com",
    output="json",
)

print(result.file.url)
use Transformy\Client;

$client = new Client(getenv('TRANSFORMY_KEY'));

$result = $client->pdf->chrome([
    'url'    => 'https://example.com',
    'output' => 'json',
]);

echo $result->file->url;
client := transformy.New(os.Getenv("TRANSFORMY_KEY"))

res, _ := client.PDF.Chrome(transformy.Params{
    URL:    "https://example.com",
    Output: "json",
})

fmt.Println(res.File.URL)
require "transformy"

client = Transformy::Client.new(ENV["TRANSFORMY_KEY"])

result = client.pdf.chrome(
  url: "https://example.com",
  output: "json"
)

puts result.file.url
Transformy client = new Transformy(System.getenv("TRANSFORMY_KEY"));

PdfResult result = client.pdf().chrome(
    ChromeParams.builder()
        .url("https://example.com")
        .output("json")
        .build());

System.out.println(result.file().url());
FIDELITY

Our PDF API renders exactly like the browser.

Real headless Chromium renders your flexbox, grid, web fonts and print CSS the way you designed them with one simple API call.

curl -X POST https://api.transformy.io/v1/pdf/chrome \
  -H "Authorization: Bearer tfy_live_..." \
  -d '{
    "url": "https://example.com/invoice/123",
    "wait_for_selector": "#invoice-ready",
    "wait_until": "networkidle"
  }'
import { Transformy } from "transformy";

const client = new Transformy(process.env.TRANSFORMY_KEY);

const { file } = await client.pdf.chrome({
  url: "https://example.com/invoice/123",
  wait_for_selector: "#invoice-ready",
  wait_until: "networkidle",
  output: "json",
});

console.log(file.url);
from transformy import Transformy

client = Transformy(os.environ["TRANSFORMY_KEY"])

result = client.pdf.chrome(
    url="https://example.com/invoice/123",
    wait_for_selector="#invoice-ready",
    wait_until="networkidle",
    output="json",
)

print(result.file.url)
use Transformy\Client;

$client = new Client(getenv('TRANSFORMY_KEY'));

$result = $client->pdf->chrome([
    'url'               => 'https://example.com/invoice/123',
    'wait_for_selector' => '#invoice-ready',
    'wait_until'        => 'networkidle',
    'output'            => 'json',
]);

echo $result->file->url;
client := transformy.New(os.Getenv("TRANSFORMY_KEY"))

res, _ := client.PDF.Chrome(transformy.Params{
    URL:             "https://example.com/invoice/123",
    WaitForSelector: "#invoice-ready",
    WaitUntil:       "networkidle",
    Output:          "json",
})

fmt.Println(res.File.URL)
require "transformy"

client = Transformy::Client.new(ENV["TRANSFORMY_KEY"])

result = client.pdf.chrome(
  url: "https://example.com/invoice/123",
  wait_for_selector: "#invoice-ready",
  wait_until: "networkidle",
  output: "json"
)

puts result.file.url
Transformy client = new Transformy(System.getenv("TRANSFORMY_KEY"));

PdfResult result = client.pdf().chrome(
    ChromeParams.builder()
        .url("https://example.com/invoice/123")
        .waitForSelector("#invoice-ready")
        .waitUntil("networkidle")
        .output("json")
        .build());

System.out.println(result.file().url());
DYNAMIC CONTENT

SPAs and JS-rendered pages, handled.

React, Vue and Svelte apps, client-side charts, and content that only appears after a fetch — we run the JavaScript, wait for it to settle, then capture the finished DOM.

wait_for_selectorWait until a CSS selector appears in the DOM.
wait_for_expressionResolve on any JS predicate you define.
inject_jsRun your own script before capture.
delayA fixed settle delay when you need it.
SCALE

Scale without the infra or the double charges.

No browser farm to run, no queue to babysit. Concurrency grows with your traffic and idles back down, and finished PDFs can go straight to your own cloud storage.

curl -X POST https://api.transformy.io/v1/pdf/chrome \
  -H "Authorization: Bearer tfy_live_..." \
  -d '{
    "url": "https://example.com/report/123",
    "async": true,
    "webhook_url": "https://yourapp.com/hooks/transformy"
  }'

# 202 Accepted — job queued, signed webhook fires on completion
{ "id": "job_7d2mfa", "status": "queued" }
import { Transformy } from "transformy";

const client = new Transformy(process.env.TRANSFORMY_KEY);

const job = await client.pdf.chrome({
  url: "https://example.com/report/123",
  async: true,
  webhook_url: "https://yourapp.com/hooks/transformy",
});

console.log(job.id, job.status);
from transformy import Transformy

client = Transformy(os.environ["TRANSFORMY_KEY"])

job = client.pdf.chrome(
    url="https://example.com/report/123",
    async_=True,
    webhook_url="https://yourapp.com/hooks/transformy",
)

print(job.id, job.status)
use Transformy\Client;

$client = new Client(getenv('TRANSFORMY_KEY'));

$job = $client->pdf->chrome([
    'url'         => 'https://example.com/report/123',
    'async'       => true,
    'webhook_url' => 'https://yourapp.com/hooks/transformy',
]);

echo $job->id;
client := transformy.New(os.Getenv("TRANSFORMY_KEY"))

job, _ := client.PDF.Chrome(transformy.Params{
    URL:        "https://example.com/report/123",
    Async:      true,
    WebhookURL: "https://yourapp.com/hooks/transformy",
})

fmt.Println(job.ID, job.Status)
require "transformy"

client = Transformy::Client.new(ENV["TRANSFORMY_KEY"])

job = client.pdf.chrome(
  url: "https://example.com/report/123",
  async: true,
  webhook_url: "https://yourapp.com/hooks/transformy"
)

puts job.id
Transformy client = new Transformy(System.getenv("TRANSFORMY_KEY"));

Job job = client.pdf().chrome(
    ChromeParams.builder()
        .url("https://example.com/report/123")
        .async(true)
        .webhookUrl("https://yourapp.com/hooks/transformy")
        .build());

System.out.println(job.id() + " " + job.status());
Scale
asyncGo sync for the file inline, or async with a signed webhook via webhook_url for big jobs.
idempotency_keyA retried request never produces a duplicate PDF or a second charge.
outputReturn the raw file, a hosted URL (json), base64 or use storage and we'll send the PDF straight to your own S3, GCS, Azure or R2 bucket via a safe presigned URL.
CONTROL

Total control of the document.

Every knob a real document needs is a plain parameter — page geometry, running headers with automatic page numbers, and how you want the result handed back.

curl -X POST https://api.transformy.io/v1/pdf/chrome \
  -H "Authorization: Bearer tfy_live_..." \
  -d '{
    "url": "https://example.com/statement",
    "page_size": "a4",
    "margin": { "top": "2cm", "bottom": "2cm" },
    "header": { "html": "Q3 Financial Report" },
    "footer": { "html": "Page {{page}} of {{pages}}" },
    "output": "json"
  }'
import { Transformy } from "transformy";

const client = new Transformy(process.env.TRANSFORMY_KEY);

const { file } = await client.pdf.chrome({
  url: "https://example.com/statement",
  page_size: "a4",
  margin: { top: "2cm", bottom: "2cm" },
  header: { html: "Q3 Financial Report" },
  footer: { html: "Page {{page}} of {{pages}}" },
  output: "json",
});

console.log(file.url);
from transformy import Transformy

client = Transformy(os.environ["TRANSFORMY_KEY"])

result = client.pdf.chrome(
    url="https://example.com/statement",
    page_size="a4",
    margin={"top": "2cm", "bottom": "2cm"},
    header={"html": "Q3 Financial Report"},
    footer={"html": "Page {{page}} of {{pages}}"},
    output="json",
)

print(result.file.url)
use Transformy\Client;

$client = new Client(getenv('TRANSFORMY_KEY'));

$result = $client->pdf->chrome([
    'url'       => 'https://example.com/statement',
    'page_size' => 'a4',
    'margin'    => ['top' => '2cm', 'bottom' => '2cm'],
    'header'    => ['html' => 'Q3 Financial Report'],
    'footer'    => ['html' => 'Page {{page}} of {{pages}}'],
    'output'    => 'json',
]);

echo $result->file->url;
client := transformy.New(os.Getenv("TRANSFORMY_KEY"))

res, _ := client.PDF.Chrome(transformy.Params{
    URL:      "https://example.com/statement",
    PageSize: "a4",
    Margin:   transformy.Margin{Top: "2cm", Bottom: "2cm"},
    Header:   transformy.HeaderFooter{HTML: "Q3 Financial Report"},
    Footer:   transformy.HeaderFooter{HTML: "Page {{page}} of {{pages}}"},
    Output:   "json",
})

fmt.Println(res.File.URL)
require "transformy"

client = Transformy::Client.new(ENV["TRANSFORMY_KEY"])

result = client.pdf.chrome(
  url: "https://example.com/statement",
  page_size: "a4",
  margin: { top: "2cm", bottom: "2cm" },
  header: { html: "Q3 Financial Report" },
  footer: { html: "Page {{page}} of {{pages}}" },
  output: "json"
)

puts result.file.url
Transformy client = new Transformy(System.getenv("TRANSFORMY_KEY"));

PdfResult result = client.pdf().chrome(
    ChromeParams.builder()
        .url("https://example.com/statement")
        .pageSize("a4")
        .margin(Margin.of("2cm", "2cm"))
        .header(HeaderFooter.html("Q3 Financial Report"))
        .footer(HeaderFooter.html("Page {{page}} of {{pages}}"))
        .output("json")
        .build());

System.out.println(result.file().url());
Document controls 3 options
page_setupPage size, orientation, margins, zoom and page ranges.
mediaEmulate print or screen media.
inject_css inject_js

Inject custom CSS & JS

Patch a page you don’t control: hide a cookie banner, restyle for print, or run a script right before capture.

"inject_css": ".cookie-bar{display:none}",
"inject_js":  "expandAllRows()"
header footer

Custom headers & footers

Full-HTML running headers and footers, with automatic tokens for page number, total pages, date and document title.

"footer": { "html": "Page {{page}} of {{pages}}" }
PRICING

Simple, usage-based billing

Start with free, no credit card required.

FREE
$0
100 documents / month
100 documents / month
Basic support
Start free
MOST POPULAR
PRO
$99/ month
10,000 documents / month
10,000 documents / month
CSS & JS injection
Async jobs & webhooks
$0.01 per additional document
Get API key

Generate your first PDF

Free for your first 100 documents a month. No credit card required.

transformy
© 2014-2026 transformy.io — made in 🇧🇪 with ❤️ and 🍟