Kamy

by Unknown

Not rated
Website

About

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.

Details

Author
Unknown
Categories
Developer Tools, Other, API

Install the SDK, paste your API key, render a PDF, sign it with PAdES, share the verify URL — or POST a PDF and get back structured JSON your agent can prove it read (seeKamy Ingest). The whole document layer for AI-native software, behind one developer-first API. Quickstart below — full reference further down the page.

pnpm add @kamydev/sdk # then add KAMY_API_KEY=kamy_pk_... to your .env
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", total: 1500, currency: "USD", from: { name: "Acme Corp" }, to: { name: "Client Inc" }, lineItems: [{ description: "Consulting", quantity: 10, unitPrice: 150, amount: 1500 }], }, }); console.log(pdf.url); // signed URL, open in your browser

{ id, url, bytes, durationMs, templateId, createdAt }

Theurlis a signed link valid for 1 hour. Open it in your browser, or stream the bytes by re-fetching it server-side. Want a permanent link? Re-mint viaGET /v1/renders/{id}any time.

List recent renders for the authenticated account withGET /v1/renders. Each entry has astatusfield that's one of"success","pending", or"failed", plusbytes,durationMs,cost, and the originatingtemplateId/templateName. Supports?pageand?pageSizefor pagination.

Stuck? Each step has a deeper section below — or skip ahead toBackend usage,raw REST, orerror handling.

The TypeScript SDK works in Node.js, Deno, Bun, and any modern server runtime.

npm install @kamydev/sdk # or pnpm add @kamydev/sdk # or yarn add @kamydev/sdk

Keep the key on yourserver only. Never ship it in client-side code.

Scopes.Each key carries a list of scopes that gate which operations it can perform. The dashboard form defaults to all scopes checked; uncheck the ones you don't need to limit blast radius if a key leaks. Available scopes:render,renders:read,templates:read,templates:write,signatures:read,signatures:write,webhooks:read,webhooks:write,schedules:read,schedules:write,uploads:read,uploads:write. A request that hits an endpoint requiring a scope the key doesn't have returns403 SCOPE_REQUIRED. Keys minted before the scope feature shipped have an empty list and bypass the check (back-compat).

Therenderscope gates every PDF-producing endpoint:/v1/render,/v1/render/async,/v1/render/bulk,/v1/render-html,/v1/render-docx,/v1/render-xlsx,/v1/render-pptx,/v1/merge,/v1/convert,/v1/pdfs/edit, and/v1/renders/{id}/split. Strip this scope from a key intended only for read-only inspection (renders:read+templates:read) so a leak can't consume your render quota.

Management scopes follow the same pattern:schedules:writegates create/update/delete on/v1/schedules;webhooks:writeon/v1/webhooks(incl. the test-delivery endpoint);uploads:writeon/v1/uploads(POST + DELETE);templates:writeon/v1/templates/{id}, the publish/rollback/versions endpoints, and thePOST /v1/templatescreator;signatures:writeon every state-mutating signature endpoint (envelopes, signature requests, reminders, signature-templates).

The mirror:readscopes gate the corresponding GET endpoints.renders:readcovers the renders list/detail and the extract/pages/jobs sub-resources;templates:readcovers template + version reads;schedules:read,webhooks:read,uploads:read, andsignatures:readcover the matching list/detail surfaces. The public anonymous catalog (GET /v1/templateswith no Authorization header) bypasses scopes entirely — it's IP-rate-limited at the network layer.

The provenance surface has its own pair.attestgatesPOST /v1/attest(sign an artifact hash);attestations:readis reserved for authenticated reads of your own attestations.trace:recordadditionally gatesPOST /v1/agent-actionsandPOST /v1/mcp/verify-server(both write ledger rows), andtrace:readgatesGET /v1/provenanceandPOST /v1/mcp/scan-tool-description.GET /v1/attest/verifytakes no key at all — it's the public recipient-facing check, IP-rate-limited and stripped of anything identifying. Full reference on theTrace & Attest page.

Render an invoice in three lines of TypeScript.

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

Inside an API route (Next.js, Hono, Express, Fastify, Nest) — render on demand and return the URL.

// app/api/invoice/route.ts (Next.js) import Kamy from "@kamydev/sdk"; const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY! }); 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 }); }

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.

// 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"); }

If you're not in a JS environment, call the REST endpoint directly.

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 }

Response headers includeX-Kamy-Cache: hitwhen the PDF was served from the response cache (seeResponse caching) andX-Kamy-Cache: missotherwise. Both hits and misses count against your render quota and bill identically — only compute time differs.

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.

Reference a template byslug. Each has a fully typed data schema (seeInvoiceData,ReceiptData, etc. exported from the SDK). Every registered system template is discoverable viaGET /v1/templatesandGET /v1/templates/{slug}regardless of whether anyone on your account has rendered it yet — the catalogue self-mirrors on first read, with a per-slug fallback so a single bad row in the seed never hides the rest.

GET /v1/templatesis also reachablewithout an API key— anonymous callers receive the public catalogue (system templates plus customs explicitly flaggedis_public: true), gated by a 30 req / min / IP rate limit. Authenticated callers additionally see their own custom templates and use the standard byKey / byUser limits. This makes the catalogue safe tocurlfrom a docs page or a discovery agent without first signing up.

Upload your own HTML/Handlebars templates from theTemplatespage — or push them straight from CI with thekamy pushCLI so your production templates stay in lockstep with your repo on every commit.

# 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
// 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 UUIDor a slugas its first argument — no need to GET-then-PATCH-by-id. Same fordeleteTemplate().

// 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 passingoptions.validateOnly: true.

Asset uploads (large images, fonts, logos)

Render requests are capped at6 MBof JSON body. Anything larger (high-resolution photos, multi-page brochure imagery, bundled font files) should be uploaded once viacreateUpload()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-signedPUTURL, stream the file body straight to storage, then embed the returnedpublicUrl— or the shorthandkamy://asset/<id>URI — anywhere in your render data. The render route resolveskamy://URIs to fresh signed URLs automatically, so you never have to manage signed-URL expiry yourself.

WhatexpiresAtmeans.TheexpiresAttimestamp returned byPOST /v1/uploadsapplies to the pre-signeduploadUrlonly — thatPUTURL is single-use and valid for 15 minutes.The asset itself never expires: once thePUTsucceeds and the row flips tostatus: "uploaded", thekamy://asset/<id>reference is rendered for the lifetime of the asset (deletable viaDELETE /v1/uploads/{id}). You can safely cache thekamy://reference indefinitely on your side and reuse it across as many renders as you like — every render mints a fresh short-lived signed download URL internally, so you do not need to re-upload to keep refs valid.

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 is100 MBper object. If a render request still hits the 6 MB limit after switching to uploads, you'll get a structured413 PAYLOAD_TOO_LARGEwith the exact limit and remediation hint in the error body.

Recurring deliveries.POST/api/v1/schedulesto set up a cron-driven render that fires on your timetable and lands as a PDF in your renders log, an inbox, or a WhatsApp chat. Every 5 minutes a worker picks up due schedules, renders the template against the saveddata, and dispatches via the configured channel.

curl -X POST https://kamy.dev/api/v1/schedules \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Weekly Acme invoice", "template": "invoice", "data": { "invoiceNumber": "INV-WEEKLY", "currency": "USD" // }, "channel": "email", "recipients": ["[email protected]"], "schedule": "0 9   1", // every Monday 09:00 "timezone": "Asia/Dubai" }'

Channels:email(via Resend),whatsapp(via Meta Cloud API — requiresWHATSAPP_PHONE_NUMBER_ID+WHATSAPP_ACCESS_TOKENon the deployment), ordownload(no delivery; the render lands in your renders log for the dashboard / API to fetch).

Minimum interval.Every plan can create schedules, but how often one may fire is capped:Free 60 minutes,Starter 15 minutes,Pro / Business / Scale 5 minutes(the cron worker's tick). A cron expression that fires more often than your plan allows is rejected at create time with422 VALIDATION_ERRORnaming the observed interval. Schedules already created are unaffected if the limits change.

Branding.Scheduled renders go through the same brand-kit auto-merge as directPOST /v1/rendercalls — your saved logo, accent color, font, and footer text are folded into the rendered PDF without any extra configuration. Setdata.brandon the schedule to override individual fields, or leave it empty to use whatever's in/dashboard/brand-kit. Free-tier accounts also receive the sameGenerated by Kamyfooter on scheduled output thatPOST /v1/renderapplies — schedules are no longer a free-tier branding bypass.

Manage viaGET /api/v1/schedules,PATCH /api/v1/schedules/{id}, andDELETE /api/v1/schedules/{id}, or visit/dashboard/schedulesfor a UI with cron presets, recipient validation, and a live preview of the next firings.

Zapier / Make / n8n.Kamy works with every no-code automation platform that can hit a REST endpoint with a Bearer token — no special connector required. Configure a custom webhook action in your tool of choice, point it atPOST https://kamy.dev/api/v1/render, set theAuthorizationheader toBearer YOUR_KAMY_API_KEY, and pass{ template, data }as the body. The response gives you a signed PDF URL you can pipe into the next step (Slack, Drive, Email, S3 — anything that takes a URL). Combine with Kamy webhooks (render.completed) to fire downstream actions when an async render finishes.

For long-running renders, fire-and-forget jobs, and bulk pipelines.

// 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();

Batch response codes— each item inresultsis either a render object or an{ error: { code, message } }shape (discriminate with"error" in item). The HTTP status reflects the aggregate outcome:200all succeeded,207partial success,502all failed. Always iterateresults— never assume a 2xx means every item rendered.

Batch scope & time budget.POST /v1/batchdrives the same pipeline as/v1/renderand therefore requires the samerenderscope on the API key. Items are rendered sequentially inside a 300-second function budget; if a long batch would run past it, the remaining items come back as{ error: { code: "SERVICE_UNAVAILABLE" } }entries with a 207 rather than the whole request timing out. Those items were never rendered and are not billed — retry just them, in a smaller batch.

Async + scheduled output parity./v1/render/asyncand scheduled renders go through the same asset-inlining and watermark pipeline as/v1/render. Remote`sources and Google Fonts<link>tags are inlined server-side before Chromium runs, eliminating the network-stall surface that used to make async + cron renders slower than their sync counterparts. Free-tier accounts also get the same per-cycleGenerated by Kamyfooter behavior across all three paths.

Bulk / mail-merge— for the classic CSV → many-PDFs flow (one template, many rows of data),POST /api/v1/render/bulkreturns a single ZIP archive instead of an array of URLs. Capped at 25 rows per call so it fits inside the platform's wall-clock budget; for larger batches call/v1/batchdirectly and chunk client-side.

curl -X POST https://kamy.dev/api/v1/render/bulk \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": "invoice", "rows": [ { "data": { "invoiceNumber": "INV-001", "from": {...}, "to": {...}, "lineItems": [...], "total": 1500, "currency": "USD" }, "name": "acme-q1" }, { "data": { "invoiceNumber": "INV-002", "from": {...}, "to": {...}, "lineItems": [...], "total": 2250, "currency": "USD" }, "name": "globex-q1" } ] }' \ --output bulk.zip # Response headers: X-Bulk-Total, X-Bulk-Rendered, X-Bulk-Failed # ZIP contains one .pdf per success + manifest.json. Failed rows # land as <name>.error.json so partial bulks remain salvageable.

HTML output— when you want the same template + data pipeline piped into transactional email (Resend, SendGrid, Mailchimp) instead of a PDF,POST /api/v1/render-htmlskips the Chromium pipeline and returns the rendered HTML as a string. Counts as one render against your monthly quota.

curl -X POST https://kamy.dev/api/v1/render-html \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": "invoice", "data": { "invoiceNumber": "INV-001" // } }' # → { "format": "html", "html": "<!DOCTYPE html>…", "bytes": 12480 }

Same template-driven mental model as PDF, different output. Spec-based for v1 — pass a structured JSON body and Kamy emits the file. One render per call counts against your monthly quota; no per-format limits.

XLSXPOST /api/v1/render-xlsxwith one or more sheets, each declaring columns + rows. Returns the workbook as binary. Headers are always auto-styled (bold + light-grey fill) — there is no per-column flag for it; passtotalRowfor a pinned bottom row (string keywordsSUM/AVG/COUNT/MIN/MAXauto- expand to=SUM(D2:D6)-style formulas;=-prefixed strings pass through verbatim), or per-columnnumFmtfor currency / percent / date formatting.

curl -X POST https://kamy.dev/api/v1/render-xlsx \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -o invoices.xlsx \ -d '{ "title": "Invoices · Q2", "sheets": [{ "name": "Open invoices", "columns": [ { "header": "Invoice #", "key": "id" }, { "header": "Customer", "key": "customer", "width": 32 }, { "header": "Issued", "key": "issued", "numFmt": "yyyy-mm-dd" }, { "header": "Amount", "key": "amount", "numFmt": "#,##0.00" } ], "rows": [ { "id": "INV-001", "customer": "Acme Inc", "issued": "2026-04-01", "amount": 1500 }, { "id": "INV-002", "customer": "Globex Co.", "issued": "2026-04-10", "amount": 3200 } ], "totalRow": { "id": "Total", "amount": "SUM" } }] }'

PPTXPOST /api/v1/render-pptxwith an array of slides, each tagged with one of the v1 layouts:title,bullets,two-column,table,quote. Passtheme.accentHex+ an optionaltheme.fontFacefor branding.

curl -X POST https://kamy.dev/api/v1/render-pptx \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -o weekly-update.pptx \ -d '{ "title": "Q2 weekly · 2026-04-29", "format": "WIDE", "theme": { "accentHex": "var(--paper-primary)" }, "slides": [ { "layout": "title", "title": "Q2 weekly", "subtitle": "Engineering · 2026-04-29" }, { "layout": "bullets", "title": "What shipped", "bullets": ["Tier 1 + 2 of expansion plan", "5 new system templates", "Schedules + WhatsApp surface"] }, { "layout": "table", "title": "Render volume", "headers": ["Plan", "This week", "MoM"], "rows": [["Free","8.2k","+12%"], ["Starter","41k","+18%"], ["Scale","112k","+22%"]] } ] }'

Stored like every other render.Both routes write arendersrow and keep the file, so the quota charge has an audit trail and the render shows up in your renders log. The id comes back on every response in theX-Kamy-Render-Idheader, and adding?response=jsonreturns the same envelope as the rest of therender-family —{ id, url, bytes, durationMs, format, filename }— instead of the raw bytes. Feed that id toPOST /v1/convertwhen the next step needs a PDF; merge, split and signature all take PDFs only.

Send any rendered PDF for signature, capture the drawn signature on a public link, and get the stamped PDF emailed back to both parties. Visual signatures (canvas drawing, not PKI) — same legal weight as a hand-drawn signature on a printed contract. Both the invite and the signed-copy notification are sent as branded HTML with your account name and the document title so the recipient sees who's asking and what they're signing instead of a bare URL paste.

# Option A — render first, then sign RENDER_ID=$(curl -s -X POST https://kamy.dev/api/v1/render \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "template": "mutual-nda", "data": { // } }' | jq -r .id) curl -X POST https://kamy.dev/api/v1/signatures \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "renderId": "'$RENDER_ID'", "signerEmail": "[email protected]", "signerName": "Jane Smith", "message": "Looking forward to working together." }' # Option B — sign an existing PDF directly (no render step) curl -X POST https://kamy.dev/api/v1/signatures \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "pdfUrl": "https://storage.example.com/contract-v3.pdf", "signerEmail": "[email protected]", "signerName": "Jane Smith" }' # → { id, sign_url, sign_token, expires_at, ... }

PassrenderId(existing render) orpdfUrl(any publicly reachable PDF — Kamy fetches and stores it server-side). Optionally passposition: { page, x, y, w, h }in PDF points (origin bottom-left) for precise placement; default is bottom-right of the last page. PasssignOnEveryPage: trueto stamp the recipient's single drawn signature on every page of the source PDF — common for multi-page B2B contracts. PassrequireStamp: trueto require a company stamp / seal in addition to the personal signature — the recipient uploads a stamp image at sign time and the server composites both onto the PDF (UAE, KSA, JP, KR, IN, CN B2B workflows). Fetch a single request withGET /api/v1/signatures/{id}, list all withGET /api/v1/signatures, cancel withPATCH /api/v1/signatures/{id}({ "action": "void" }), and resend the invite withPOST /api/v1/signatures/{id}/remind(rate-limited to once per hour — returns HTTP 429 withRetry-Afterif too soon). PassreminderCadenceHourson the create call (24–168) to auto-remind on a schedule — the worker resends the invite every N hours while pending, up to 3 reminders total. For higher-value transactions (real estate, employment, financial) passauthMethod: "email_otp"— the sign page renders an OTP gate before the document loads, the signer enters a 6-digit code we email tosignerEmail, document unlocks on verify (uses the same Resend channel as the invite, no extra env). SMS OTP is also supported viaauthMethod: "sms_otp"+signerPhone(E.164) and requiresTWILIO_ACCOUNT_SID,TWILIO_AUTH_TOKEN, andTWILIO_PHONE_NUMBERin env — currently on hold from the dashboard UI but live on the API. Firessignature.openedon first sign-page load andsignature.voidedon void. For sales-team workflows that fan one rendered PDF out to a list of signers (NDAs, MSAs, onboarding agreements), usePOST /api/v1/signatures/bulkwith up to 100 signers in a single request — each row produces an independent signature_requests row + invite email and the response returns per-row success / failure with HTTP 207 when any row failed. The batch is reserved against your signature quota atomically up front, so a batch that would exceed a Free plan's monthly signature allowance is rejected in full withQUOTA_EXCEEDED(402) rather than partially dispatched. Every terminal signature request (signed, declined, delegated, voided, expired) exposes aCertificate of CompletionPDF — fetch viaGET /api/v1/signatures/{id}/certificate(Bearer auth,signatures.readscope) or token-auth at/api/sign/{token}/certificate. The PDF records the full lifecycle (invite → opened → consent → signed/declined/delegated, with timestamps, IP, user agent, ESIGN/UETA consent acknowledgment) and cross-links to the cryptographic/verify/{sha256}page when the signed PDF was PAdES-sealed. Manage from/dashboard/signatures.

For flat PDFs (Word exports, scanned contracts) that don't ship AcroForm widgets, attachplacedFieldson the create request — the signer page renders fillable inputs at the configured PDF coordinates (origin bottom-left, points), and the server stamps submitted values onto the page before applying the signature. Field types:text,textarea,checkbox,date,initials,radio,dropdown. Passoptions: ["…"]for radio/dropdown to constrain choices. Up to 100 fields per request, names must be unique.

If a field carriessourcePageWidth/sourcePageHeight(the viewer size you measured the coordinates against), the server rescalesx/y/w/hto the real page size. When it cannot — the source PDF could not be downloaded or parsed — the coordinates are stored exactly as supplied and the 201 response carries awarningsentry saying so. Treat that warning as "verify the placement": check it withPOST /api/v1/signatures/preview-placement, which returns the real page sizes and flags fields that fall outside the page.

Templates can carry their own default signing config — setsignature_position,stamp_position,placed_fields,requires_stamp, andsign_on_every_pageon the template row and every signature request created against a render of that template inherits them automatically. Lets you place a signature box once on an NDA template and have every send reuse it. Precedence: request body →signatureTemplateId→ template defaults → server bottom-right fallback.

curl -X POST https://kamy.dev/api/v1/signatures \ -H "Authorization: Bearer $KAMY_API_KEY" \ -d '{ "renderId": "'$RENDER_ID'", "signerEmail": "[email protected]", "signerName": "Jane Smith", "expiresIn": 604800, "ccEmails": ["[email protected]"], "placedFields": [ { "name": "fullName", "type": "text", "page": 1, "x": 100, "y": 600, "w": 220, "h": 22, "required": true, "signerLabel": "Your full legal name" }, { "name": "initials", "type": "initials", "page": 1, "x": 400, "y": 600, "w": 60, "h": 22 }, { "name": "jurisdiction", "type": "dropdown", "page": 1, "x": 100, "y": 560, "w": 180, "h": 22, "options": ["England & Wales", "New York", "UAE DIFC"] }, { "name": "agreeTerms", "type": "checkbox", "page": 1, "x": 100, "y": 520, "w": 18, "h": 18, "required": true } ] }'

Add ananchorstring to any placed field and the server locates the matching text in the PDF and positions the field there. Pairanchorwithx/yto offset from the match. PasssignatureTemplateId(fromPOST /api/v1/signature-templates) to apply a reusable default set of placedFields, position, message, expiresIn, and ccEmails — per-request fields always override template defaults. The merged request is re-validated against the same request schema (bounds, type checks) before the signing pipeline runs, so a template row that predates stricter validation cannot bypass current input rules.

Formulti-signer flowsusePOST /api/v1/envelopesinstead. Supply 2–10 recipients; each gets an independent sign link against the same source PDF (parallel routing). The envelope status becomescompletedwhen the last recipient signs. Void all pending requests in one call withPATCH /api/v1/envelopes/{id}({ "action": "void" }) — firessignature.voidedper recipient andsignature.envelope_completed/signature.envelope_voidedat the envelope level. Token expiry defaults to 30 days; passexpiresIn(seconds, 3 600–2 592 000) to override. CC up to 10 observer addresses viaccEmails— they receive the invite copy and the signed-PDF notification. The signed PDF lands in your renders log alongside any other render.

Send one PDF to 2–10 recipients simultaneously. Each gets an independent sign link; the envelope status becomescompletedwhen the last recipient signs. Userouting: "sequential"to gate each invite behind the previous signer — recipient 2 receives their link only after recipient 1 completes.

curl -X POST https://kamy.dev/api/v1/envelopes \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "renderId": "'$RENDER_ID'", "routing": "parallel", "message": "Please review and sign.", "expiresIn": 604800, "ccEmails": ["[email protected]"], "recipients": [ { "email": "[email protected]", "name": "Alice Smith", "order": 1 }, { "email": "[email protected]", "name": "Bob Jones", "order": 2 } ] }' # → { envelope: { id, status: "pending", routing, … }, recipients: [{ sign_url, … }, …] } # Void all pending requests in one call: curl -X PATCH https://kamy.dev/api/v1/envelopes/$ENVELOPE_ID \ -H "Authorization: Bearer $KAMY_API_KEY" \ -d '{ "action": "void" }'

Fetch envelope status + all recipients withGET /api/v1/envelopes/{id}. List paginated withGET /api/v1/envelopes. Voiding firessignature.voidedper affected recipient andsignature.envelope_voidedat the envelope level. Completion firessignature.envelope_completed.

Store reusable defaults —placedFields,position,message,expiresIn,ccEmails— and reference the template by ID on any signature request. Per-request fields always override template defaults.

# Create a template curl -X POST https://kamy.dev/api/v1/signature-templates \ -H "Authorization: Bearer $KAMY_API_KEY" \ -d '{ "name": "NDA — standard", "message": "Please sign the attached NDA.", "expiresIn": 604800, "ccEmails": ["[email protected]"], "placedFields": [ { "name": "fullName", "type": "text", "page": 1, "x": 80, "y": 650, "w": 220, "h": 22, "anchor": "Signatory name", "required": true } ] }' # → { id: "tpl_…", name, placed_fields, … } # Use it in a signature request curl -X POST https://kamy.dev/api/v1/signatures?preview=1 \ -H "Authorization: Bearer $KAMY_API_KEY" \ -d '{ "renderId": "'$RENDER_ID'", "signerEmail": "[email protected]", "signerName": "Jane Smith", "signatureTemplateId": "'$TPL_ID'" }'

Manage templates withGET /api/v1/signature-templates(list),GET /api/v1/signature-templates/{id}(detail),PATCH /api/v1/signature-templates/{id}(update), andDELETE /api/v1/signature-templates/{id}.

Seal a render with an X.509 certificate so any tampering breaks the signature. Output is an ETSI EN 319 142-1 PAdES-B-LT signature: the basic seal (B-B), an RFC 3161 timestamp from a public TSA embedded as an unsigned attribute on the SignerInfo (B-T), and a CRL revocation snapshot embedded in the PKCS#7 SignedData so verifiers can check revocation offline (B-LT). Signed under Kamy's in-house CA — recipients verify authenticity at kamy.dev/verify.

# Seal an existing render with a PAdES X.509 signature. curl -X POST https://kamy.dev/api/v1/sign/$RENDER_ID \ -H "Authorization: Bearer $KAMY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "reason": "Approved by Finance", "location": "Dubai, UAE", "withTimestamp": true }' # → { signed_pdf_url, signed_pdf_sha256, cert_id, # timestamped, has_revocation_info, verify_url, ... }`
No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.