API docs
Base URL: https://api.slipkiln.com. All requests and responses are JSON, except render endpoints, which return application/pdf.
Quickstart
- Get a free API key (50 documents/month).
- Save it in your shell:
export SLIPKILN_KEY=sk_… - Send HTML or JSON and get a PDF back.
curl -X POST https://api.slipkiln.com/v1/render/template \
-H "Authorization: Bearer $SLIPKILN_KEY" \
-H "Content-Type: application/json" \
-d '{"template":"invoice","data":{"number":"INV-1001","currency":"USD",
"customer":{"name":"Acme Co."},
"items":[{"description":"Consulting","qty":10,"unit_price":120}]}}' \
-o invoice.pdfconst res = await fetch("https://api.slipkiln.com/v1/render/template", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SLIPKILN_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
template: "invoice",
data: { number: "INV-1001", currency: "USD", customer: { name: "Acme Co." },
items: [{ description: "Consulting", qty: 10, unit_price: 120 }] },
}),
});
if (!res.ok) throw new Error((await res.json()).message);
await require("fs/promises").writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));import os, requests
r = requests.post(
"https://api.slipkiln.com/v1/render/template",
headers={"Authorization": f"Bearer {os.environ['SLIPKILN_KEY']}"},
json={"template": "invoice",
"data": {"number": "INV-1001", "currency": "USD", "customer": {"name": "Acme Co."},
"items": [{"description": "Consulting", "qty": 10, "unit_price": 120}]}},
timeout=30,
)
r.raise_for_status()
open("invoice.pdf", "wb").write(r.content)Authentication
Send your key as a bearer token: Authorization: Bearer sk_…. Keys are shown once at signup and stored only as a hash, so we can't recover a lost key. Email support@slipkiln.com if you lose yours. Keep keys server-side. Don't ship them in browser or mobile code.
Create a key with POST /v1/signup {"email":"you@company.com"} → 201 {"api_key":"sk_…","plan":"free","monthly_limit":50}. One account per email.
POST/v1/render/html
Render your own HTML/CSS with a headless Chromium.
| Field | Type | |
|---|---|---|
html | string, required | Full HTML document or fragment (≤ 2 MB). Images, fonts and CSS must be inlined as data: URIs. External URLs are blocked. |
options | object | See render options. |
curl -X POST https://api.slipkiln.com/v1/render/html \
-H "Authorization: Bearer $SLIPKILN_KEY" -H "Content-Type: application/json" \
-d '{"html":"<h1 style=\"color:#0f766e\">Hello PDF</h1>","options":{"format":"Letter","margin":{"top":"20mm"}}}' \
-o hello.pdfconst res = await fetch("https://api.slipkiln.com/v1/render/html", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.SLIPKILN_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ html: "<h1>Hello PDF</h1>", options: { format: "Letter" } }),
});
const pdf = Buffer.from(await res.arrayBuffer());r = requests.post("https://api.slipkiln.com/v1/render/html",
headers={"Authorization": f"Bearer {os.environ['SLIPKILN_KEY']}"},
json={"html": "<h1>Hello PDF</h1>", "options": {"format": "Letter"}}, timeout=30)
open("hello.pdf", "wb").write(r.content)Successful responses include x-usage-month: documents rendered by your account this month.
POST/v1/render/template
{"template": "invoice" | "receipt" | "certificate", "data": {…}, "options": {…}}. All text is HTML-escaped. GET /v1/templates (no auth) returns every template with full example data.
Templates
Shared brand object (all templates):
brand.name | string | Shown when no logo is given |
brand.logo | string | Base64 data:image/png|jpeg|gif|webp|svg+xml;base64,… only |
brand.color | string | Hex accent color, e.g. #0f766e |
brand.email, brand.website | string | Contact lines |
brand.address | string[] | Address lines |
invoice (A4)
number | string | Invoice number |
issue_date, due_date | string | Printed as given |
currency | string | ISO 4217, default USD |
customer | object | name, email, address (string[]) |
items | array | {description, qty, unit_price}, max 500. Totals are computed. |
tax_rate | number | Percent, e.g. 8.25 |
notes | string | Footer note |
receipt (A5)
number, date | string | |
payment_method | string | e.g. "Visa •••• 4242" |
currency, tax_rate, items | Same as invoice | |
customer.name | string |
certificate (A4 landscape)
title | string | e.g. "Certificate of Completion" |
recipient | string | Name |
description | string | Body text |
date | string | |
signer | object | name, title |
certificate_id | string | Printed bottom-right |
Render options
format | A4 | A5 | Letter | Legal |
landscape | boolean |
margin | {top,right,bottom,left}, values like 10mm, 0.5in, 20px |
print_background | boolean, default true |
javascript | boolean, default false (HTML endpoint only; network stays blocked) |
Account & billing
GET /v1/me | Plan and usage this month |
POST /v1/billing/checkout {"plan":"starter|growth|scale"} | Returns {url} to Stripe Checkout |
POST /v1/billing/change-plan {"plan":…} | Switch paid plans (prorated) |
POST /v1/billing/portal | Returns {url} to the Stripe billing portal (card, invoices, cancel) |
Or use the Billing page.
Errors
Errors are JSON: {"error":"code","message":"…"}.
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request, invalid_template, invalid_email, invalid_plan | Bad input |
| 401 | unauthorized | Missing/invalid key |
| 402 | quota_exceeded | Free plan's 50 docs used this month |
| 408 | render_timeout | Render took over 15 s |
| 409 | email_exists, already_subscribed | Conflict |
| 413 | render_too_large / body too large | Request > 2 MB or PDF > 20 MB |
| 422 | render_failed | Chromium could not render |
| 429 | Rate limited; retry after the retry-after header | |
| 503 | render_busy | Temporarily at capacity; retry with backoff |
Limits & security
- 60 requests/minute per key; signup 5/hour per IP.
- Free: 50 successful documents per calendar month (UTC). Failed renders don't count. Paid plans: included documents, then per-document overage (see pricing).
- Request body ≤ 2 MB; PDF ≤ 20 MB; 15 s per render.
- Rendering runs in an isolated Chromium with no network access: only
data:URIs load. JavaScript is off unless you setoptions.javascript. - Documents are processed in memory and not stored after delivery.