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).

SlugNameDescription
invoiceInvoiceLine items, tax, discounts, payment terms.
receiptReceiptCompact thermal-style, monospace amounts.
quoteQuoteValidity date, quote number, line items.
contractContractNumbered sections, signature blocks.
shipping-labelShipping Label4×6 inch with barcode and tracking.
certificateCertificateDecorative border, signature line.
reportReportCover, TOC, auto headers/footers.
agreementAgreementOne-page with parties and terms.
uae-tax-invoiceUAE Tax InvoiceFTA-compliant bilingual AR/EN, 5% VAT.
ksa-zatca-invoiceKSA ZATCA InvoiceZATCA 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, and render.started 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" | "pro" | "team"
console.log(me.limits.rendersPerMonth);  // number, or null for unlimited
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);        // 10_000 (Pro plan; null on unlimited tiers)
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
  }
}
CodeHTTPDescription
UNAUTHORIZED401Missing or invalid API key
INVALID_API_KEY401Key format invalid
API_KEY_REVOKED401Key was revoked
FORBIDDEN403Access denied
NOT_FOUND404Template or render not found
VALIDATION_ERROR422Data doesn't match template schema
RATE_LIMITED429Too many requests
QUOTA_EXCEEDED402Monthly render limit reached
PAYLOAD_TOO_LARGE413Request body exceeded the 6 MB JSON limit
RENDER_FAILED500PDF 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?

500 free renders every month.

Create a free accountGet your API key

Servidores relacionados

NotebookLM Web Importer

Importe páginas da web e vídeos do YouTube para o NotebookLM com um clique. Confiado por mais de 200.000 usuários.

Instalar extensão do Chrome