@kamydev/sdk
v0.6.1
Published
TypeScript SDK for the Kamy PDF generation API
Maintainers
Readme
@kamydev/sdk
TypeScript SDK for the Kamy PDF generation API. Works in Node, Bun, Deno, Cloudflare Workers, Vercel Edge, and the browser.
Installation
```bash npm install @kamydev/sdk
or pnpm add @kamydev/sdk
```
Quick start
```typescript import Kamy from "@kamydev/sdk";
const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY ?? "" });
// Render an invoice — `data` is fully type-checked against the InvoiceData schema. 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"] }, to: { name: "Client Inc", address: ["456 Oak Ave"] }, lineItems: [{ description: "Consulting", quantity: 10, unitPrice: 150, amount: 1500 }], subtotal: 1500, total: 1500, currency: "USD", }, });
await pdf.toFile("./invoice.pdf"); // save to disk const bytes = await pdf.toBuffer(); // or get the bytes const stream = await pdf.toStream(); // or stream them ```
What's new in 0.6.1
E-signature is now a typed first-class resource. Take any rendered PDF and turn it into a sign-and-return flow:
const pdf = await kamy.render({ template: "contract", data: { ... } });
const sig = await kamy.signatures.create({
renderId: pdf.id,
signerEmail: "[email protected]",
signerName: "Alice Example",
// optional — defaults to bottom-right of last page sized 220×64 pt
position: { page: 3, x: 360, y: 80, w: 220, h: 64 },
// optional — included in the email invitation when RESEND_API_KEY is set
message: "Please review and sign by Friday.",
});
console.log(sig.sign_url); // share with the signer
console.log(sig.sign_token); // path-component equivalent// List recent requests with pagination.
const { signatures, total } = await kamy.signatures.list({ limit: 25 });Returns are fully typed via SignatureRequest / SignatureRequestCreated.
Status flows pending → signed | voided | expired; tokens auto-expire
after 30 days.
What's new in 0.6.0
26 system templates across 10 packs, 6 new endpoints, recurring schedules, and
public forms — all reachable through the same kamy.render() mental model.
New templates
// MENA
await kamy.render({ template: "uae-tax-invoice", data: { /* … */ } });
await kamy.render({ template: "ksa-zatca-invoice", data: { /* … */ } });
await kamy.render({ template: "bh-vat-invoice", data: { /* … */ } }); // VAT 10%
await kamy.render({ template: "om-vat-invoice", data: { /* … */ } }); // VAT 5%
await kamy.render({ template: "eg-vat-invoice", data: { /* … */ } }); // VAT 14%
await kamy.render({ template: "qa-commercial-invoice", data: { /* … */ } });
await kamy.render({ template: "kw-commercial-invoice", data: { /* … */ } });
// Global compliance
await kamy.render({ template: "uk-vat-invoice", data: { /* … */ } }); // HMRC
await kamy.render({ template: "eu-vat-invoice", data: { /* … */ } }); // Directive 2006/112/EC
await kamy.render({ template: "us-w9", data: { /* … */ } });
await kamy.render({ template: "us-1099-nec", data: { /* … */ } });
await kamy.render({ template: "in-gst-invoice", data: { /* … */ } });
await kamy.render({ template: "au-tax-invoice", data: { /* … */ } });
// UAE verticals + legal
await kamy.render({ template: "uae-tenancy-contract", data: { /* … */ } });
await kamy.render({ template: "uae-noc-letter", data: { /* … */ } });
await kamy.render({ template: "uae-offer-letter", data: { /* … */ } });
await kamy.render({ template: "salary-certificate", data: { /* … */ } });
await kamy.render({ template: "mutual-nda", data: { /* … */ } });New endpoints (HTTP-callable today; typed methods land in 0.6.x patches)
POST /v1/render/bulk # 1 template × N rows → ZIP of N PDFs (cap 25)
POST /v1/render-html # rendered HTML for Resend / SendGrid / Mailchimp
POST /v1/render-xlsx # spec-based XLSX (sheets / columns / rows / totalRow)
POST /v1/render-pptx # spec-based PowerPoint (5 layouts + theme)
POST /v1/schedules # recurring renders on a cron + email/whatsapp/downloadPublic forms — no SDK call required, just share the URL:
https://kamy.dev/forms/<your-slug>What's new in 0.2.x
0.2.2 — cache-hit visibility
Cache hits now expose explicit fields in the JSON body, not just a header:
const result = await kamy.render({
template: "invoice",
data: { /* ... */ },
idempotencyKey: "order-9f3a-attempt-1",
});
if (result.cached) {
// Replayed from the 24h idempotency cache. result.originalRenderId
// is the id of the original render whose response is being returned.
metrics.increment("kamy.quota_savings");
}Equivalent to the existing result.idempotencyHit (header-derived) but
visible to anyone reading raw response bodies — analytics pipelines,
data warehouses, webhook fan-out, etc.
0.2.1 — types polish
RenderRequesttype alias exported —RenderTemplateOptions<TemplateSlug> | RenderCustomTemplateOptions. Use it as the natural argument shape forkamy.render().- Explicit
idempotencyKey?: stringre-declared on every render-options interface. Functionally identical to the inheritedRequestOptionsfield — re-declared so editors surface it in object-literal autocomplete on the call site. updateTemplate(idOrSlug, patch)anddeleteTemplate(idOrSlug)already accepted slugs in 0.2.0 — now loudly documented. No more GET-by-slug-then-PATCH-by-id dance.
All 0.2.x additions are backwards-compatible with 0.2.0 and 0.1.x.
What's new in 0.2.0
| Feature | Method | |---|---| | Save / stream PDF directly | `pdf.toBuffer() / toFile() / toStream()` | | Idempotency keys (no double-renders) | `render({ ..., idempotencyKey: "..." })` | | Async rendering with polling | `renderAsync(...).wait()` | | Render raw HTML | `renderHtml({ html: "..." })` | | Render a URL | `renderUrl({ url: "https://..." })` | | Batch up to 100 PDFs | `renderBatch([...])` | | Merge existing PDFs | `merge([id1, id2, ...])` | | Webhook CRUD + signature verification | `kamy.webhooks.*` + `verifyWebhook()` | | Per-template typed sugar | `kamy.invoice({ data: ... })` | | Honors `Retry-After` on 429/503 | automatic | | AbortSignal pass-through | `render({ ..., signal })` | | Request lifecycle hooks | `new Kamy({ onRequest, onResponse, onRetry })` | | Sandbox / test mode | `apiKey: "kamy_test_..."` | | `KamyError.requestId`, `.retryAfter`, `.isRetryable` | always populated | | Edge / browser / Bun / Deno safe User-Agent | automatic |
All additions are backwards-compatible with 0.1.x.
Configuration
```typescript const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY ?? "", baseUrl: "https://kamy.dev", // default timeout: 30_000, // ms maxRetries: 2, // network + 429 + 5xx fetch: customFetch, // optional onRequest: (ctx) => log("→", ctx.method, ctx.url), onResponse: (ctx) => log("←", ctx.status, ctx.requestId), onRetry: (ctx) => log("retry", ctx.attempt, ctx.delayMs, "ms"), }); ```
Render methods
Sync
```typescript // Built-in template (typed data) await kamy.render({ template: "receipt", data: { /* ReceiptData */ } });
// Custom template (loose data) await kamy.render({ template: "tpl_abc123", data: { foo: "bar" } });
// Raw HTML await kamy.renderHtml({ html: "Hello", options: { format: "letter" } });
// URL await kamy.renderUrl({ url: "https://example.com" });
// Sugar shortcuts (same options as `render`) await kamy.invoice({ data: { /* InvoiceData / } }); await kamy.shippingLabel({ data: { / ShippingLabelData */ } }); ```
Async + polling
```typescript const job = await kamy.renderAsync({ template: "invoice", data }); const result = await job.wait({ pollMs: 1000, timeoutMs: 60_000, onProgress: (snap) => console.log(snap.status), }); console.log(result.url); ```
Batch
```typescript const result = await kamy.renderBatch([ { template: "invoice", data: invoiceA }, { template: "invoice", data: invoiceB }, { html: "Cover sheet" }, ]); console.log(result.succeeded, "/", result.total); ```
Merge
```typescript const merged = await kamy.merge([renderId1, renderId2, renderId3]); console.log(merged.url); ```
Idempotency
Pass an `idempotencyKey` to make any render safe to retry. Within 24 hours, identical retries return the cached response (and `pdf.idempotencyHit === true`).
```typescript const pdf = await kamy.render({ template: "invoice", data, idempotencyKey: `order-${orderId}`, }); ```
Webhooks
```typescript // CRUD const created = await kamy.webhooks.create({ url: "https://example.com/hook", events: ["render.completed", "render.failed"], }); console.log("Save this:", created.secret); // shown ONCE
const all = await kamy.webhooks.list(); await kamy.webhooks.update(id, { enabled: false }); await kamy.webhooks.delete(id); ```
Signature verification (works in any runtime — Node, Edge, Workers, Deno):
```typescript import { verifyWebhook } from "@kamydev/sdk";
export async function POST(req: Request): Promise { try { const event = await verifyWebhook({ payload: await req.text(), signature: req.headers.get("x-kamy-signature") ?? "", secret: process.env.KAMY_WEBHOOK_SECRET ?? "", }); // event.event, event.data, event.timestamp return new Response("ok"); } catch { return new Response("invalid signature", { status: 400 }); } } ```
Error handling
```typescript import Kamy, { KamyError } from "@kamydev/sdk";
try { await kamy.render({ template: "invoice", data }); } catch (err) { if (err instanceof KamyError) { console.error(err.code); // "QUOTA_EXCEEDED" console.error(err.status); // 402 console.error(err.requestId); // for support tickets console.error(err.retryAfter); // seconds, on 429/503 console.error(err.isRetryable); // true for 429, network, 5xx } } ```
Cancellation
```typescript const ctrl = new AbortController(); setTimeout(() => ctrl.abort(), 5_000);
const pdf = await kamy.render({ template: "invoice", data, signal: ctrl.signal, }); ```
Test mode
Use a key starting with `kamy_test_` to render in sandbox mode (no quota, no billing).
```typescript const kamy = new Kamy({ apiKey: "kamy_test_..." }); ```
License
MIT
