Kamy
Kamy renders invoices, receipts, contracts, and 5 more production-grade templates with a single REST call or TypeScript SDK method. No headless browser. No DevOps.
01
Install the SDK
The TypeScript SDK works in Node.js, Deno, Bun, and any modern server runtime.
bash
npm install @kamydev/sdk
# or
pnpm add @kamydev/sdk
# or
yarn add @kamydev/sdk
02
Get an API key
Sign in to the dashboard, open API Keys, click New key. Copy the key once — it's shown only on creation.
Environment variable
KAMY_API_KEY=kamy_pk_...
Keep the key on your server only. Never ship it in client-side code.
03
Quick start
Render an invoice in three lines of TypeScript.
ts
import Kamy from "@kamydev/sdk";
const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY! });
const pdf = await kamy.render({
template: "invoice",
data: {
invoiceNumber: "INV-001",
issueDate: "2026-01-01",
dueDate: "2026-01-31",
from: { name: "Acme Corp", address: ["123 Main St", "SF, CA"] },
to: { name: "Client Inc", address: ["456 Oak Ave", "NY, NY"] },
lineItems: [
{ description: "Consulting", quantity: 10, unitPrice: 150, amount: 1500 },
],
subtotal: 1500,
total: 1500,
currency: "USD",
},
});
console.log(pdf.url); // signed URL, valid 1 hour
04
Using from a Node backend
Inside an API route (Next.js, Hono, Express, Fastify, Nest) — render on demand and return the URL.
ts
// app/api/invoice/route.ts (Next.js)
import Kamy from "@kamydev/sdk";
export async function POST(req: Request) {
const body = await req.json();
const pdf = await kamy.render({ template: "invoice", data: body });
return Response.json({ url: pdf.url });
}
05
Using from the frontend
Important: API keys must never be embedded in frontend code. Call your own backend, which calls Kamy. The browser only sees the resulting signed URL.
ts
// client-side React
async function downloadInvoice(data: InvoiceData) {
const res = await fetch("/api/invoice", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const { url } = await res.json();
window.open(url, "_blank");
}
06
Raw REST API
If you're not in a JS environment, call the REST endpoint directly.
bash
curl -X POST https://kamy.dev/api/v1/render \
-H "Authorization: Bearer $KAMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "invoice",
"data": { "invoiceNumber": "INV-001", "total": 1500, "currency": "USD" }
}'
Response: { id, url, bytes, durationMs, templateId, createdAt }
Prefer Postman, Insomnia, or Bruno? Import the official collection — postman/kamy.json — covers all 23 v1 routes with sample bodies, path-variable defaults, and Bearer auth pre-wired to a single {{apiKey}} collection variable.
07
Built-in templates
Reference a template by slug. Each has a fully typed data schema (see InvoiceData, ReceiptData, etc. exported from the SDK).
| Slug | Name | Description |
|---|---|---|
| invoice | Invoice | Line items, tax, discounts, payment terms. |
| receipt | Receipt | Compact thermal-style, monospace amounts. |
| quote | Quote | Validity date, quote number, line items. |
| contract | Contract | Numbered sections, signature blocks. |
| shipping-label | Shipping Label | 4×6 inch with barcode and tracking. |
| certificate | Certificate | Decorative border, signature line. |
| report | Report | Cover, TOC, auto headers/footers. |
| agreement | Agreement | One-page with parties and terms. |
| uae-tax-invoice | UAE Tax Invoice | FTA-compliant bilingual AR/EN, 5% VAT. |
| ksa-zatca-invoice | KSA ZATCA Invoice | ZATCA Phase-1 simplified, TLV QR. |
08
Custom Handlebars templates
Upload your own HTML/Handlebars templates from the Templates page — or push them straight from CI with the kamy push CLI so your production templates stay in lockstep with your repo on every commit.
bash
# CI: idempotent upsert keyed on slug, safe to re-run
npm i -g @kamydev/cli
export KAMY_API_KEY=kamy_pk_...
kamy push templates/invoice.hbs --css templates/invoice.css --tag finance
ts
// Or from the SDK
await kamy.pushTemplate({
slug: "invoice-acme", // creates if missing, updates if present
name: "Acme Invoice",
html: await fs.readFile("invoice.hbs", "utf8"),
css: await fs.readFile("invoice.css", "utf8"),
});
// Then render by slug just like a built-in
await kamy.render({
template: "invoice-acme",
data: { orderId: "123", items: [/* … */] },
});
Patching an existing template? updateTemplate() accepts either a UUID or a slug as its first argument — no need to GET-then-PATCH-by-id. Same for deleteTemplate().
ts
// Slug-keyed PATCH — single round trip
await kamy.updateTemplate("invoice-acme", {
name: "Acme Invoice (Q2 redesign)",
html: updatedHbs,
});
// Slug-keyed DELETE
await kamy.deleteTemplate("invoice-acme");
Handlebars helpers available: currency, date, add, number, plus all built-ins (each, if, unless). Validate payloads against your schema in CI without burning credits by passing options.validateOnly: true.
09
Asset uploads (large images, fonts, logos)
Render requests are capped at 6 MB of JSON body. Anything larger (high-resolution photos, multi-page brochure imagery, bundled font files) should be uploaded once via createUpload() and then referenced from your template by URL — same render call, fraction of the bytes on the wire.
Uploads use a two-step pattern: ask Kamy for a pre-signed PUT URL, stream the file body straight to storage, then embed the returned publicUrl — or the shorthand kamy://asset/<id> URI — anywhere in your render data. The render route resolves kamy:// URIs to fresh signed URLs automatically, so you never have to manage signed-URL expiry yourself.
ts
import { readFile } from "node:fs/promises";
// 1. Ask Kamy for a pre-signed PUT URL (15-min single-use).
const upload = await kamy.createUpload({
filename: "hero.jpg",
contentType: "image/jpeg",
sizeBytes: 4_200_000, // optional pre-flight check vs 100 MB cap
});
// 2. Stream the file body to Supabase Storage with PUT (NOT POST).
await fetch(upload.uploadUrl, {
method: "PUT",
headers: { "Content-Type": "image/jpeg" },
body: await readFile("./hero.jpg"),
});
// 3a. Reference the long-lived publicUrl directly in your data…
await kamy.render({
template: "flyer",
data: { heroImage: upload.publicUrl },
});
// 3b. …or use the kamy:// shorthand. The render route auto-resolves
// it to a freshly-signed URL on every render, so no expiry to manage.
await kamy.render({
template: "flyer",
data: { heroImage: `kamy://asset/${upload.path.split("/").pop()}` },
});
Hard cap is 100 MB per object. If a render request still hits the 6 MB limit after switching to uploads, you'll get a structured 413 PAYLOAD_TOO_LARGE with the exact limit and remediation hint in the error body.
10
Async, batch & merge
For long-running renders, fire-and-forget jobs, and bulk pipelines.
ts
// Async — enqueue and poll
const job = await kamy.renderAsync({ template: "report", data });
const pdf = await job.wait({ pollIntervalMs: 1000, timeoutMs: 120_000 });
// Batch — up to 100 renders in one request
const { results } = await kamy.renderBatch([
{ template: "invoice", data: { invoiceNumber: "INV-001" /* … */ } },
{ template: "receipt", data: { receiptNumber: "REC-002" /* … */ } },
]);
// Merge — combine 2–20 rendered PDFs into one document
const merged = await kamy.merge([pdf1.id, pdf2.id, pdf3.id]);
// Idempotency — safe to retry without double-charging
await kamy.render({
template: "invoice",
data,
idempotencyKey: "order-12345", // any unique string up to 64 chars
});
// Download helpers
await pdf.toFile("./invoice.pdf"); // write to disk
const buf = await pdf.toBuffer();
const stream = await pdf.toStream();
11
Webhooks
Subscribe to render.completed, render.failed, batch.completed, and merge.completed events. Each delivery is signed with HMAC-SHA256 — verify with verifyWebhook before processing.
ts
// 1. Create a subscription
const hook = await kamy.webhooks.create({
url: "https://example.com/hooks/kamy",
events: ["render.completed", "render.failed"],
});
// Save hook.secret — it is shown only once.
// 2. Verify deliveries in your handler
import { verifyWebhook } from "@kamydev/sdk";
export async function POST(req: Request) {
const body = await req.text();
const sig = req.headers.get("x-kamy-signature") ?? "";
const ok = await verifyWebhook({
body,
signature: sig,
secret: process.env.KAMY_WEBHOOK_SECRET!,
});
if (!ok) return new Response("invalid signature", { status: 401 });
const event = JSON.parse(body);
// event.type, event.data.render, event.data.jobId
return new Response("ok");
}
12
Quota & usage (programmatic)
Two read-only endpoints expose plan limits and live usage so you can wire dashboards, quota meters, or CI pre-flight checks without scraping invoice emails or guessing when you'll hit the wall.
ts
// Plan + static limits — call once at app boot, cache it.
const me = await kamy.me();
console.log(me.plan); // "free" | "starter" | "pro" | "business" | "scale"
console.log(me.limits.rendersPerMonth); // number, or null for unlimited (Scale)
console.log(me.limits.customTemplates); // boolean
console.log(me.limits.seats); // number
// Live UTC-calendar-month usage — cheap, safe to poll.
const usage = await kamy.usage();
console.log(usage.renders.used); // 1_247
console.log(usage.renders.quota); // 25_000 (Pro plan; null on Scale)
console.log(usage.renders.remaining); // 8_753 (null on unlimited tiers)
console.log(usage.period.start, usage.period.end);
// Pre-flight before a bulk job
if (usage.renders.remaining !== null && usage.renders.remaining < jobs.length) {
throw new Error(`Need ${jobs.length} renders, only ${usage.renders.remaining} left.`);
}
Both endpoints are quota-free — they don't count against your monthly render budget. me() changes only on plan upgrades, so cache it; usage() is the one to poll.
13
Error handling
ts
import Kamy, { KamyError } from "@kamydev/sdk";
try {
const pdf = await kamy.render({ template: "invoice", data });
} catch (err) {
if (err instanceof KamyError) {
console.error(err.code); // e.g. "QUOTA_EXCEEDED"
console.error(err.status); // HTTP status
console.error(err.message); // Human-readable
}
}
| Code | HTTP | Description |
|---|---|---|
| UNAUTHORIZED | 401 | Missing or invalid API key |
| INVALID_API_KEY | 401 | Key format invalid |
| API_KEY_REVOKED | 401 | Key was revoked |
| FORBIDDEN | 403 | Access denied |
| NOT_FOUND | 404 | Template or render not found |
| VALIDATION_ERROR | 422 | Data doesn't match template schema |
| RATE_LIMITED | 429 | Too many requests |
| QUOTA_EXCEEDED | 402 | Monthly render limit reached |
| PAYLOAD_TOO_LARGE | 413 | Request body exceeded the 6 MB JSON limit |
| RENDER_FAILED | 500 | PDF generation failed |
14
MCP server (Claude, Cursor)
Kamy exposes an MCP server at https://mcp.kamy.dev/mcp so AI agents can render PDFs directly. Add this to your Claude Desktop or Cursor config:
json
{
"mcpServers": {
"kamy": {
"url": "https://mcp.kamy.dev/mcp",
"headers": { "Authorization": "Bearer kamy_pk_..." }
}
}
}
Available tools: render_pdf, list_templates, get_template, list_renders, install_sdk.
Ready to render?
100 free renders every month.
Create a free accountGet your API key
Похожие серверы
Alpha Vantage MCP Server
спонсорAccess financial market data: realtime & historical stock, ETF, options, forex, crypto, commodities, fundamentals, technical indicators, & more
TeamCity
MCP server for TeamCity, integrates with Claude Desktop and Cursor.
Alchemy MCP Server
Interact with Alchemy's blockchain APIs to query data without writing code.
MCP Code Executor
Allows LLMs to execute Python code within a specified and configurable Python environment.
VSCode MCP
Interact with VSCode through the Model Context Protocol, enabling AI agents to perform development tasks.
Floyd
Scheduling and booking engine for AI agents. Check availability, hold slots, and confirm appointments with two-phase booking and conflict-free resource management.
Deepseek Thinker
Provides Deepseek's reasoning capabilities to AI clients, supporting both the Deepseek API and local Ollama server modes.
Remote MCP Server (Authless)
An example of a remote MCP server deployable on Cloudflare Workers without authentication, allowing for custom tool integration.
git-mcp
A Git MCP server that doesn't suck
mcp4gql
An MCP server that acts as a bridge, allowing MCP clients to interact with a target GraphQL API.
Adobe After Effects
Control Adobe After Effects through a standardized protocol, enabling AI assistants and other applications.