Kamy
Kamy renderiza faturas, recibos, contratos e mais 5 modelos de nível de produção com uma única chamada REST ou método do SDK TypeScript. Sem navegador headless. Sem DevOps.
Documentação
Novo — Em destaque no Cursor Directory ↗
Gere, assine, comprove — em 60 segundos.
Instale o SDK, cole sua chave de API, renderize um PDF, assine com PAdES, compartilhe o URL de verificação — ou envie um PDF via POST e receba de volta JSON estruturado que seu agente pode comprovar que leu (veja Kamy Ingest). Toda a camada documental para software nativo de IA, atrás de uma API developer-first. Início rápido abaixo — referência completa mais adiante na página.
Início rápido — copie e cole
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
Você recebe de volta
{ id, url, bytes, durationMs, templateId, createdAt }
O url é um link assinado válido por 1 hora. Abra-o no navegador ou transmita os bytes re-buscando-o no servidor. Quer um link permanente? Re-emita via GET /v1/renders/{id} a qualquer momento.
Liste renderizações recentes da conta autenticada com GET /v1/renders. Cada entrada tem um campo status que é um de "success", "pending" ou "failed", além de bytes, durationMs, cost e o templateId / templateName de origem. Suporta ?page e ?pageSize para paginação.
Travou? Cada etapa tem uma seção mais detalhada abaixo — ou pule para Uso no backend, REST puro ou tratamento de erros.
01
Instale o SDK
O SDK TypeScript funciona em Node.js, Deno, Bun e qualquer runtime de servidor moderno.
npm install @kamydev/sdk
# or
pnpm add @kamydev/sdk
# or
yarn add @kamydev/sdk
02
Obtenha uma chave de API
Entre no dashboard, abra API Keys, clique em Nova chave. Copie a chave uma vez — ela só é exibida na criação.
Variável de ambiente
KAMY_API_KEY=kamy_pk_...
Mantenha a chave apenas no servidor. Nunca a envie em código client-side.
Escopos. Cada chave carrega uma lista de escopos que controlam quais operações ela pode executar. O formulário do dashboard vem com todos os escopos marcados por padrão; desmarque os que não precisar para limitar o impacto caso uma chave vaze. Escopos disponíveis: render, renders:read, templates:read, templates:write, signatures:read, signatures:write, webhooks:read, webhooks:write, schedules:read, schedules:write, uploads:read, uploads:write. Uma requisição que atinge um endpoint que exige um escopo que a chave não possui retorna 403 SCOPE_REQUIRED. Chaves criadas antes do recurso de escopos existir têm lista vazia e ignoram a verificação (compatibilidade reversa).
O escopo render controla todos os endpoints que produzem PDF: /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 e /v1/renders/{id}/split. Remova este escopo de uma chave destinada apenas a inspeção somente-leitura (renders:read + templates:read) para que um vazamento não consuma sua cota de renderização.
Escopos de gerenciamento seguem o mesmo padrão: schedules:write controla criar/atualizar/excluir em /v1/schedules; webhooks:write em /v1/webhooks (incluindo o endpoint de teste de entrega); uploads:write em /v1/uploads (POST + DELETE); templates:write em /v1/templates/{id}, os endpoints de publicar/reverter/versões e o criador de POST /v1/templates; signatures:write em todo endpoint de assinatura que altera estado (envelopes, solicitações de assinatura, lembretes, modelos de assinatura).
Os escopos espelhados *:read controlam os endpoints GET correspondentes. renders:read cobre a lista/detalhe de renderizações e os sub-recursos extract/pages/jobs; templates:read cobre leituras de modelo + versão; schedules:read, webhooks:read, uploads:read e signatures:read cobrem as superfícies de lista/detalhe correspondentes. O catálogo anônimo público (GET /v1/templates sem cabeçalho Authorization) ignora escopos completamente — é limitado por IP na camada de rede.
A superfície de proveniência tem seu próprio par. attest controla POST /v1/attest (assinar um hash de artefato); attestations:read é reservado para leituras autenticadas de suas próprias atestações. trace:record adicionalmente controla POST /v1/agent-actions e POST /v1/mcp/verify-server (ambos gravam linhas no ledger), e trace:read controla GET /v1/provenance e POST /v1/mcp/scan-tool-description. GET /v1/attest/verify não exige chave alguma — é a verificação pública voltada ao destinatário, limitada por IP e sem qualquer dado identificável. Referência completa na página Trace & Attest.
03
Início rápido
Renderize uma fatura em três linhas de 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
04
Uso a partir de um backend Node
Dentro de uma rota de API (Next.js, Hono, Express, Fastify, Nest) — renderize sob demanda e retorne o 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 });
}
05
Uso a partir do frontend
Importante: chaves de API nunca devem ser embutidas em código frontend. Chame seu próprio backend, que chama o Kamy. O navegador só vê o URL assinado resultante.
// 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
API REST pura
Se você não está em um ambiente JS, chame o endpoint REST diretamente.
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" }
}'
Resposta: { id, url, bytes, durationMs, templateId, createdAt }
Os cabeçalhos de resposta incluem X-Kamy-Cache: hit quando o PDF foi servido do cache de resposta (veja Cache de resposta) e X-Kamy-Cache: miss caso contrário. Tanto acertos quanto erros contam contra sua cota de renderização e são cobrados igualmente — apenas o tempo de computação difere.
Prefere Postman, Insomnia ou Bruno? Importe a coleção oficial — postman/kamy.json — cobre todas as 23 rotas v1 com exemplos de corpo, padrões de variáveis de caminho e autenticação Bearer pré-configurada para uma única variável de coleção {{apiKey}}.
07
Modelos integrados
Referencie um modelo por slug. Cada um tem um esquema de dados totalmente tipado (veja InvoiceData, ReceiptData, etc. exportados do SDK). Todo modelo de sistema registrado é descoberto via GET /v1/templates e GET /v1/templates/{slug} independentemente de alguém na sua conta já tê-lo renderizado — o catálogo se auto-espelha na primeira leitura, com um fallback por slug para que uma única linha ruim na semente nunca esconda o resto.
GET /v1/templates também é acessível sem chave de API — chamadas anônimas recebem o catálogo público (modelos de sistema mais personalizados explicitamente marcados como is_public: true), limitado a 30 req / min / IP. Chamadas autenticadas também veem seus próprios modelos personalizados e usam os limites padrão byKey / byUser. Isso torna o catálogo seguro para curl a partir de uma página de documentação ou um agente de descoberta sem precisar se cadastrar primeiro.
| Slug | Nome | Descrição |
|---|---|---|
| invoice | Fatura | Itens de linha, impostos, descontos, condições de pagamento. |
| receipt | Recibo | Compacto estilo térmico, valores em monoespaçada. |
| quote | Orçamento | Data de validade, número do orçamento, itens de linha. |
| contract | Contrato | Seções numeradas, blocos de assinatura. |
| shipping-label | Etiqueta de Envio | 4×6 polegadas com código de barras e rastreio. |
| certificate | Certificado | Borda decorativa, linha de assinatura. |
| report | Relatório | Capa, sumário, cabeçalhos/rodapés automáticos. |
| agreement | Acordo | Uma página com partes e termos. |
| uae-tax-invoice | Fatura Fiscal dos EAU | Conforme FTA, bilíngue AR/EN, IVA de 5%. |
| ksa-zatca-invoice | Fatura ZATCA da Arábia Saudita | Simplificada Fase 1 ZATCA, QR TLV. |
08
Modelos Handlebars personalizados
Envie seus próprios modelos HTML/Handlebars pela página Templates — ou envie-os direto do CI com o CLI kamy push para que seus modelos de produção permaneçam em sincronia com seu repositório a cada 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: [/* … */] },
});
Corrigindo um modelo existente? updateTemplate() aceita um UUID ou um slug como primeiro argumento — sem necessidade de GET-e-depois-PATCH-por-id. O mesmo vale para deleteTemplate().
// 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");
Helpers Handlebars disponíveis: currency, date, add, number, além de todos os integrados (each, if, unless). Valide payloads contra seu esquema no CI sem gastar créditos passando options.validateOnly: true.
09
Uploads de ativos (imagens grandes, fontes, logotipos)
Requisições de renderização são limitadas a 6 MB de corpo JSON. Qualquer coisa maior (fotos de alta resolução, imagens de folhetos multipáginas, arquivos de fonte empacotados) deve ser enviada uma vez via createUpload() e depois referenciada pelo seu modelo por URL — mesma chamada de renderização, fração dos bytes na rede.
Uploads usam um padrão de duas etapas: peça ao Kamy um URL PUT pré-assinado, transmita o corpo do arquivo direto para o armazenamento e então incorpore o publicUrl retornado — ou a forma abreviada URI kamy://asset/<id> — em qualquer lugar dos seus dados de renderização. A rota de renderização resolve URIs kamy:// para URLs assinados atualizados automaticamente, então você nunca precisa gerenciar a expiração de URLs assinados.
O que expiresAt significa. O timestamp expiresAt retornado por POST /v1/uploads se aplica apenas ao uploadUrl pré-assinado — esse URL PUT é de uso único e válido por 15 minutos. O ativo em si nunca expira: uma vez que o PUT é concluído e a linha muda para status: "uploaded", a referência kamy://asset/<id> é renderizada pela vida útil do ativo (excluível via DELETE /v1/uploads/{id}). Você pode armazenar em cache a referência kamy:// indefinidamente no seu lado e reutilizá-la em quantas renderizações quiser — cada renderização gera internamente um novo URL assinado de download de curta duração, então você não precisa re-enviar para manter as referências válidas.
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()}\` },
});
Limite máximo é 100 MB por objeto. Se uma requisição de renderização ainda atingir o limite de 6 MB após mudar para uploads, você receberá um 413 PAYLOAD_TOO_LARGE estruturado com o limite exato e uma dica de correção no corpo do erro.
09b
Agendamentos + integrações
Entregas recorrentes. POST /api/v1/schedules para configurar uma renderização orientada por cron que dispara no seu cronograma e chega como PDF no seu log de renderizações, em uma caixa de entrada ou em um chat WhatsApp. A cada 5 minutos um worker coleta agendamentos devidos, renderiza o modelo com o data salvo e despacha pelo canal configurado.
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"
}'
Canais: email (via Resend), whatsapp (via Meta Cloud API — exige WHATSAPP_PHONE_NUMBER_ID + WHATSAPP_ACCESS_TOKEN na implantação) ou download (sem entrega; a renderização cai no seu log de renderizações para o dashboard / API buscar).
Intervalo mínimo. Todo plano pode criar agendamentos, mas a frequência de disparo é limitada: Free 60 minutos, Starter 15 minutos, Pro / Business / Scale 5 minutos (o tick do worker cron). Uma expressão cron que dispare com mais frequência do que seu plano permite é rejeitada na criação com 422 VALIDATION_ERROR nomeando o intervalo observado. Agendamentos já criados não são afetados se os limites mudarem.
Marca. Renderizações agendadas passam pela mesma mesclagem automática de brand-kit que chamadas diretas de POST /v1/render — seu logotipo salvo, cor de destaque, fonte e texto de rodapé são incorporados ao PDF renderizado sem configuração extra. Defina data.brand no agendamento para substituir campos individuais, ou deixe vazio para usar o que estiver em /dashboard/brand-kit. Contas do plano Free também recebem o mesmo rodapé Generated by Kamy na saída agendada que POST /v1/render aplica — agendamentos não são mais uma brecha de marca para o plano Free.
Gerencie via GET /api/v1/schedules, PATCH /api/v1/schedules/{id} e DELETE /api/v1/schedules/{id}, ou visite /dashboard/schedules para uma interface com predefinições de cron, validação de destinatários e uma prévia ao vivo dos próximos disparos.
Zapier / Make / n8n. O Kamy funciona com qualquer plataforma de automação no-code que consiga acessar um endpoint REST com um token Bearer — não é necessário conector especial. Configure uma ação de webhook personalizada na ferramenta de sua escolha, aponte-a para POST https://kamy.dev/api/v1/render, defina o cabeçalho Authorization como Bearer YOUR_KAMY_API_KEY e passe { template, data } como corpo. A resposta fornece uma URL de PDF assinada que você pode encaminhar para a próxima etapa (Slack, Drive, Email, S3 — qualquer coisa que aceite uma URL). Combine com os webhooks do Kamy (render.completed) para disparar ações downstream quando uma renderização assíncrona terminar.
10
Async, lote, bulk e merge
Para renderizações de longa duração, trabalhos fire-and-forget e pipelines em massa.
// 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();
Códigos de resposta em lote — cada item em results é um objeto de renderização ou uma forma { error: { code, message } } (diferencie com "error" in item). O status HTTP reflete o resultado agregado: 200 todos tiveram sucesso, 207 sucesso parcial, 502 todos falharam. Sempre itere sobre results — nunca presuma que um 2xx significa que todos os itens foram renderizados.
Escopo do lote e orçamento de tempo. POST /v1/batch aciona o mesmo pipeline que /v1/render e, portanto, exige o mesmo escopo render na chave de API. Os itens são renderizados sequencialmente dentro de um orçamento de função de 300 segundos; se um lote longo ultrapassar esse tempo, os itens restantes retornam como entradas { error: { code: "SERVICE_UNAVAILABLE" } } com um 207, em vez de a solicitação inteira expirar. Esses itens nunca foram renderizados e não são cobrados — tente novamente apenas eles, em um lote menor.
Paridade de saída assíncrona e agendada. /v1/render/async e renderizações agendadas passam pelo mesmo pipeline de inlining de ativos e marca d'água que /v1/render. Fontes remotas <img> e tags <link> do Google Fonts são incorporadas no servidor antes da execução do Chromium, eliminando a superfície de atraso de rede que costumava tornar as renderizações assíncronas e cron mais lentas que suas contrapartes síncronas. Contas do plano gratuito também recebem o mesmo comportamento de rodapé Generated by Kamy por ciclo em todos os três caminhos.
Bulk / mail-merge — para o fluxo clássico CSV → muitos PDFs (um modelo, muitas linhas de dados), POST /api/v1/render/bulk retorna um único arquivo ZIP em vez de uma matriz de URLs. Limitado a 25 linhas por chamada para caber no orçamento de tempo real da plataforma; para lotes maiores, chame /v1/batch diretamente e divida no lado do cliente.
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.
Saída HTML — quando você quiser o mesmo pipeline de modelo + dados encaminhado para e-mail transacional (Resend, SendGrid, Mailchimp) em vez de um PDF, POST /api/v1/render-html pula o pipeline do Chromium e retorna o HTML renderizado como uma string. Conta como uma renderização em sua cota mensal.
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 }
10b
Geração de XLSX e PPTX
Mesmo modelo mental orientado a modelos que o PDF, com saída diferente. Baseado em especificação para v1 — passe um corpo JSON estruturado e o Kamy emite o arquivo. Uma renderização por chamada conta contra sua cota mensal; sem limites por formato.
XLSX — POST /api/v1/render-xlsx com uma ou mais planilhas, cada uma declarando colunas + linhas. Retorna a pasta de trabalho como binário. Os cabeçalhos são sempre estilizados automaticamente (negrito + preenchimento cinza claro) — não há sinalizador por coluna para isso; passe totalRow para uma linha inferior fixa (palavras-chave de string SUM / AVG / COUNT / MIN / MAX expandem automaticamente para fórmulas estilo =SUM(D2:D6); strings prefixadas com = passam literalmente), ou numFmt por coluna para formatação de moeda / porcentagem / data.
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" }
}]
}'
PPTX — POST /api/v1/render-pptx com uma matriz de slides, cada um marcado com um dos layouts v1: title, bullets, two-column, table, quote. Passe theme.accentHex e um theme.fontFace opcional para 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%"]] }
]
}'
Armazenado como qualquer outra renderização. Ambas as rotas gravam uma linha renders e mantêm o arquivo, então a cobrança da cota tem uma trilha de auditoria e a renderização aparece no seu log de renderizações. O id retorna em cada resposta no cabeçalho X-Kamy-Render-Id, e adicionar ?response=json retorna o mesmo envelope que o restante da família render-* — { id, url, bytes, durationMs, format, filename } — em vez dos bytes brutos. Alimente esse id para POST /v1/convert quando a próxima etapa precisar de um PDF; merge, split e assinatura aceitam apenas PDFs.
10c
Assinatura eletrônica
Envie qualquer PDF renderizado para assinatura, capture a assinatura desenhada em um link público e receba o PDF carimbado por e-mail para ambas as partes. Assinaturas visuais (desenho em canvas, não PKI) — mesmo peso legal que uma assinatura manuscrita em um contrato impresso. Tanto o convite quanto a notificação da cópia assinada são enviados como HTML com marca, com o nome da sua conta e o título do documento, para que o destinatário veja quem está pedindo e o que está assinando, em vez de uma URL simples colada.
# 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, ... }
Passe renderId (renderização existente) ou pdfUrl (qualquer PDF publicamente acessível — o Kamy busca e armazena no servidor). Opcionalmente, passe position: { page, x, y, w, h } em pontos PDF (origem no canto inferior esquerdo) para posicionamento preciso; o padrão é o canto inferior direito da última página. Passe signOnEveryPage: true para carimbar a única assinatura desenhada do destinatário em todas as páginas do PDF de origem — comum em contratos B2B de várias páginas. Passe requireStamp: true para exigir um carimbo / selo da empresa além da assinatura pessoal — o destinatário envia uma imagem de carimbo no momento da assinatura e o servidor compõe ambos no PDF (fluxos B2B de UAE, KSA, JP, KR, IN, CN). Busque uma única solicitação com GET /api/v1/signatures/{id}, liste todas com GET /api/v1/signatures, cancele com PATCH /api/v1/signatures/{id} ({ "action": "void" }) e reenvie o convite com POST /api/v1/signatures/{id}/remind (limitado a uma vez por hora — retorna HTTP 429 com Retry-After se for cedo demais). Passe reminderCadenceHours na chamada de criação (24–168) para lembrete automático em um cronograma — o worker reenvia o convite a cada N horas enquanto pendente, até 3 lembretes no total. Para transações de maior valor (imobiliário, emprego, financeiro), passe authMethod: "email_otp" — a página de assinatura exibe um portão OTP antes do carregamento do documento, o signatário insere um código de 6 dígitos que enviamos por e-mail para signerEmail, o documento é desbloqueado na verificação (usa o mesmo canal Resend do convite, sem env extra). OTP por SMS também é suportado via authMethod: "sms_otp" + signerPhone (E.164) e requer TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN e TWILIO_PHONE_NUMBER no env — atualmente em espera na interface do dashboard, mas ativo na API. Dispara signature.opened no primeiro carregamento da página de assinatura e signature.voided em anulação. Para fluxos de equipe de vendas que distribuem um PDF renderizado para uma lista de signatários (NDAs, MSAs, acordos de integração), use POST /api/v1/signatures/bulk com até 100 signatários em uma única solicitação — cada linha produz uma linha independente de signature_requests + e-mail de convite, e a resposta retorna sucesso / falha por linha com HTTP 207 quando qualquer linha falha. O lote é reservado atomicamente contra sua cota de assinaturas antecipadamente, então um lote que excederia a cota mensal de assinaturas de um plano Free é rejeitado por completo com QUOTA_EXCEEDED (402) em vez de ser parcialmente despachado. Cada solicitação de assinatura terminal (assinada, recusada, delegada, anulada, expirada) expõe um Certificado de Conclusão em PDF — busque via GET /api/v1/signatures/{id}/certificate (auth Bearer, escopo signatures.read) ou token-auth em /api/sign/{token}/certificate. O PDF registra o ciclo de vida completo (convite → aberto → consentimento → assinado/recusado/delegado, com timestamps, IP, user agent, reconhecimento de consentimento ESIGN/UETA) e faz referência cruzada à página criptográfica /verify/{sha256} quando o PDF assinado foi selado com PAdES. Gerencie em /dashboard/signatures.
Para PDFs planos (exportações do Word, contratos digitalizados) que não possuem widgets AcroForm, anexe placedFields na solicitação de criação — a página do signatário renderiza campos preenchíveis nas coordenadas PDF configuradas (origem no canto inferior esquerdo, pontos), e o servidor carimba os valores enviados na página antes de aplicar a assinatura. Tipos de campo: text, textarea, checkbox, date, initials, radio, dropdown. Passe options: ["…"] para radio/dropdown para restringir escolhas. Até 100 campos por solicitação, os nomes devem ser únicos.
Se um campo carregar sourcePageWidth / sourcePageHeight (o tamanho do visualizador em que você mediu as coordenadas), o servidor redimensiona x/y/w/h para o tamanho real da página. Quando não for possível — o PDF de origem não pôde ser baixado ou analisado — as coordenadas são armazenadas exatamente como fornecidas e a resposta 201 carrega uma entrada warnings informando isso. Trate esse aviso como "verifique o posicionamento": confira com POST /api/v1/signatures/preview-placement, que retorna os tamanhos reais das páginas e sinaliza campos que ficam fora da página.
Os modelos podem carregar sua própria configuração de assinatura padrão — defina signature_position, stamp_position, placed_fields, requires_stamp e sign_on_every_page na linha do modelo e toda solicitação de assinatura criada contra uma renderização desse modelo os herda automaticamente. Permite colocar uma caixa de assinatura uma vez em um modelo de NDA e fazer com que cada envio a reutilize. Precedência: corpo da solicitação → signatureTemplateId → padrões do modelo → fallback do servidor no canto inferior direito.
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 }
]
}'
Adicione uma string anchor a qualquer campo posicionado e o servidor localiza o texto correspondente no PDF e posiciona o campo ali. Combine anchor com x / y para deslocar da correspondência. Passe signatureTemplateId (de POST /api/v1/signature-templates) para aplicar um conjunto padrão reutilizável de placedFields, posição, mensagem, expiresIn e ccEmails — campos por solicitação sempre substituem os padrões do modelo. A solicitação mesclada é revalidada contra o mesmo esquema de solicitação (limites, verificações de tipo) antes do pipeline de assinatura ser executado, então uma linha de modelo anterior a validações mais rígidas não pode contornar as regras de entrada atuais.
Para fluxos com vários signatários, use POST /api/v1/envelopes em vez disso. Forneça 2–10 destinatários; cada um recebe um link de assinatura independente contra o mesmo PDF de origem (roteamento paralelo). O status do envelope se torna completed quando o último destinatário assina. Anule todas as solicitações pendentes em uma chamada com PATCH /api/v1/envelopes/{id} ({ "action": "void" }) — dispara signature.voided por destinatário e signature.envelope_completed / signature.envelope_voided no nível do envelope. A expiração do token é de 30 dias por padrão; passe expiresIn (segundos, 3 600–2 592 000) para substituir. Copie até 10 endereços de observadores via ccEmails — eles recebem a cópia do convite e a notificação do PDF assinado. O PDF assinado chega ao seu log de renderizações junto com qualquer outra renderização.
10d
Envelopes com vários signatários
Envie um PDF para 2–10 destinatários simultaneamente. Cada um recebe um link de assinatura independente; o status do envelope se torna completed quando o último destinatário assina. Use routing: "sequential" para condicionar cada convite ao signatário anterior — o destinatário 2 recebe seu link somente após o destinatário 1 concluir.
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" }'
Busque o status do envelope + todos os destinatários com GET /api/v1/envelopes/{id}. Liste paginado com GET /api/v1/envelopes. Anular dispara signature.voided por destinatário afetado e signature.envelope_voided no nível do envelope. A conclusão dispara signature.envelope_completed.
10e
Modelos de assinatura
Armazene padrões reutilizáveis — placedFields, position, message, expiresIn, ccEmails — e referencie o modelo por ID em qualquer solicitação de assinatura. Campos por solicitação sempre substituem os padrões do modelo.
# 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'"
}'
Gerencie modelos com GET /api/v1/signature-templates (lista), GET /api/v1/signature-templates/{id} (detalhe), PATCH /api/v1/signature-templates/{id} (atualização) e DELETE /api/v1/signature-templates/{id}.
10f
Selagem criptográfica (PAdES)
Sele uma renderização com um certificado X.509 para que qualquer adulteração quebre a assinatura. A saída é uma assinatura ETSI EN 319 142-1 PAdES-B-LT: o selo básico (B-B), um timestamp RFC 3161 de uma TSA pública incorporado como atributo não assinado no SignerInfo (B-T) e um snapshot de revogação CRL incorporado no PKCS#7 SignedData para que verificadores possam verificar a revogação offline (B-LT). Assinado sob a CA interna do Kamy — os destinatários verificam a autenticidade em 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, ... }
Passe withTimestamp: false para pular o round-trip do TSA (PAdES-B-B em vez de B-T) — útil para renders offline / air-gapped. O verify_url da resposta resolve para kamy.dev/verify, onde qualquer destinatário pode arrastar e soltar o PDF assinado — o SHA-256 é calculado no navegador via SubtleCrypto (o arquivo nunca é enviado) e então comparado com o ledger de signing_events para revelar a identidade do signatário, a cadeia de certificados e o timestamp embutido.
O ponto de distribuição de CRL em cada certificado folha resolve para https://kamy.dev/api/verify/crl — Acrobat e openssl o seguem automaticamente quando não há CRL embutida. CLI: kamy sign <render-id> encapsula o mesmo endpoint; kamy verify <file.pdf> calcula o hash de um PDF local e imprime sua URL de verificação sem chamada de API.
10e
Utilitários de documento — converter, dividir, extrair texto e páginas
Quatro endpoints que funcionam em qualquer render já existente na sua biblioteca (ou aceitam novos arquivos diretamente). GET /api/v1/renders/{id}/extract e o rasterizador de páginas não consomem cota. POST /api/v1/convert é cobrado como um render, e POST /api/v1/renders/{id}/split como um render por intervalo — uma divisão em quatro intervalos custa quatro. Qualquer coisa que produza um novo render na sua biblioteca conta como um render; veja Cota e uso.
Não é o mesmo que Kamy Ingest. Estes utilitários extraem texto ou valores de campos AcroForm de um render que a Kamy já produziu. Para extrair JSON estruturado de um PDF arbitrário recebido (fatura de fornecedor, formulário de sinistro, contrato que você recebeu) com uma URL pública de verificação, veja Kamy Ingest — endpoint diferente, superpoder diferente.
Converter DOCX / XLSX / CSV → PDF
Envie um documento Word, planilha ou CSV e receba um RenderResult padrão. Usa mammoth para fidelidade de DOCX e SheetJS para tabelas de planilhas — sem dependência de LibreOffice do seu lado.
curl -X POST https://kamy.dev/api/v1/convert \
-H "Authorization: Bearer $KAMY_API_KEY" \
-F "[email protected]" \
-F "name=Q3 Contract"
# → { id, url, bytes, durationMs, name, createdAt }
Dividir por intervalo de páginas
Divida um render em N novos renders — um por intervalo. Os intervalos são baseados em 1 e inclusivos. Omita to para estender até a última página. Até 50 intervalos por chamada.
const { renders } = await kamy.split({
id: "rnd_abc",
ranges: [
{ from: 1, to: 3, name: "Cover + Terms" },
{ from: 4, name: "Appendix" }, // to the end
],
});
// renders[0].url — signed URL for pages 1-3
Extrair texto ou campos AcroForm de um render
Funciona com um render já existente na sua biblioteca — GET /api/v1/renders/{id}/extract. Endpoint diferente de POST /api/v1/extract (Kamy Ingest, que aceita qualquer PDF recebido e retorna JSON estruturado + URL de verificação).
type=text retorna blocos de texto por página além de uma string fullText. type=fields retorna nomes, tipos e valores atuais dos campos AcroForm — útil para auditar formulários preenchidos.
const text = await kamy.extract({ id: "rnd_abc" });
// text.fullText — joined plaintext
// text.pages — [{ page: 1, text: "…" }, …]
const fields = await kamy.extract({ id: "rnd_abc", type: "fields" });
// fields.fields — [{ name: "signatureDate", type: "text", value: "2025-01-01" }]
Rasterizar páginas para PNG
Cada página vira uma URL PNG assinada (expira em 1 hora). Padrão de 150 DPI para pré-visualizações em tela; passe dpi=300 para miniaturas em qualidade de impressão. As imagens são armazenadas em pdfs/{userId}/pages/{renderId}/ e sobrescritas em chamadas repetidas.
const { pages } = await kamy.renderPages({ id: "rnd_abc", dpi: 150 });
// pages[0] — { page: 1, width: 1240, height: 1754, url: "https://…" }
10g
Edição de PDF — preencher, carimbar e redigir
Edite um PDF existente antes de enviá-lo para assinatura — preencha campos AcroForm, carimbe texto em coordenadas exatas ou pinte caixas de redação opacas sobre conteúdo sensível. O resultado é salvo como um novo render que você pode passar diretamente para POST /api/v1/signatures ou POST /api/v1/envelopes — e, por ser um novo render, conta como um contra sua cota mensal. Passar pdfUrl em vez de renderId custa dois: a fonte buscada é armazenada como um render próprio para que a edição tenha algo para referenciar.
Forneça um renderId (um render existente na sua biblioteca) ou um pdfUrl (qualquer PDF publicamente acessível — a Kamy busca, edita e armazena). Até 50 operações por chamada, aplicadas em ordem.
Operações
fill_field— escreve um valor em um widget AcroForm nomeado (texto, checkbox, rádio, dropdown). PasseflattenFields: true(padrão) para gravar os valores na página para que não sejam mais editáveis.stamp_text— desenha uma string de texto em uma coordenada de user-space do PDF (origem no canto inferior esquerdo). SuportafontSize,color(hex) eopacity.cover— pinta um retângulo preenchido sobre uma região. Apenas visual. O texto abaixo permanece no arquivo e ainda pode ser copiado e colado ou lido por qualquer parser de PDF; a resposta carrega um avisoCOVER_VISUAL_ONLYpor operação. Esta operação era chamada deredacte nunca redigiu nada, então foi renomeada eop: "redact"agora retorna422 REDACTION_NOT_SUPPORTED. Se um valor não deve ser recuperável, mantenha-o fora do documento de origem.
# Edit a client-uploaded lease agreement, then send for signature.
curl -X POST https://kamy.dev/api/v1/pdfs/edit \
-H "Authorization: Bearer $KAMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"pdfUrl": "https://example.com/lease-template.pdf",
"operations": [
{ "op": "fill_field", "field": "TenantName", "value": "Alice Smith" },
{ "op": "fill_field", "field": "StartDate", "value": "2026-06-01" },
{ "op": "fill_field", "field": "AgreeTerms", "value": true },
{ "op": "stamp_text", "page": 4,
"x": 72, "y": 120, "text": "Ref: LSE-2026-001",
"fontSize": 9, "color": "#888888" },
{ "op": "cover", "page": 2,
"x": 310, "y": 540, "w": 160, "h": 18 }
]
}'
# → { id, url, bytes, durationMs, name, warnings }
// SDK — fill → sign in two lines
const edited = await kamy.pdfs.edit({
pdfUrl: "https://example.com/lease-template.pdf",
operations: [
{ op: "fill_field", field: "TenantName", value: "Alice Smith" },
{ op: "fill_field", field: "StartDate", value: "2026-06-01" },
{ op: "stamp_text", page: 4, x: 72, y: 120,
text: "Ref: LSE-2026-001", fontSize: 9 },
],
});
const sig = await kamy.signatures.create({
renderId: edited.id,
signerEmail: "[email protected]",
signerName: "Alice Smith",
});
Todas as coordenadas são pontos de user-space do PDF (72 pt = 1 polegada, origem no canto inferior esquerdo) — o mesmo sistema usado por placedFields em toda a API de assinatura eletrônica. Use GET /api/v1/renders/{id}/extract?type=fields para listar os nomes dos campos AcroForm no seu PDF de origem antes de preencher.
Uma edição custa um render, seja passando renderId ou pdfUrl. Uma fonte pdfUrl é buscada, editada em memória e armazenada uma vez — ela não se torna um render separado na sua biblioteca.
11
Webhooks
Assine os eventos render.completed, render.failed, batch.completed e merge.completed. Cada entrega é assinada com HMAC-SHA256 — verifique com verifyWebhook antes de processar.
// 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
Cota e uso (programático)
Dois endpoints somente leitura expõem limites do plano e uso em tempo real para que você possa conectar dashboards, medidores de cota ou verificações pré-flight de CI sem precisar vasculhar e-mails de fatura ou adivinhar quando vai bater no limite.
// 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.\`);
}
Ambos os endpoints não consomem cota — eles não contam contra seu orçamento mensal de renders. me() só muda em upgrades de plano, então faça cache; usage() é o que deve ser consultado.
usage(), account() e a cota que a API impõe contam a mesma coisa: renders bem-sucedidos dentro do mês calendário UTC atual, independentemente do que os produziu — /v1/render, /v1/batch, /v1/merge, /v1/convert, /v1/renders/{id}/split, /v1/pdfs/edit, uma assinatura concluída, um agendamento, uma etapa de workflow ou um envio de formulário público. A regra é simplesmente que qualquer coisa que adicione um novo render à sua biblioteca custa um. Então usage.renders.remaining é exatamente quantos renders você ainda pode fazer antes de um 402 QUOTA_EXCEEDED, não uma estimativa.
Cada um desses endpoints também impõe a cota, não apenas conta contra ela: /v1/merge, /v1/pdfs/edit e /v1/renders/{id}/split retornam 402 QUOTA_EXCEEDED ao atingir o limite antes de fazer qualquer trabalho. split custa um render por intervalo e reserva o valor total antecipadamente — uma divisão em 4 intervalos em um plano com 3 renders restantes é rejeitada por completo em vez de ser parcialmente concluída.
No plano Scale medido, a mesma regra decide pelo que você é cobrado: cada render na sua biblioteca reporta uma unidade medida, independentemente de qual superfície o produziu — a API, um agendamento, uma etapa de workflow, um envio de formulário público ou o Brain. Não existe fonte de renders que conte contra o uso sem ser cobrada, ou vice-versa.
13
Tratamento de erros
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 | Chave de API ausente ou inválida |
| INVALID_API_KEY | 401 | Formato de chave inválido |
| API_KEY_REVOKED | 401 | A chave foi revogada |
| FORBIDDEN | 403 | Acesso negado |
| SCOPE_REQUIRED | 403 | A chave de API não possui o escopo necessário para esta operação |
| NOT_FOUND | 404 | Template, render ou solicitação de assinatura não encontrado |
| VALIDATION_ERROR | 422 | O corpo da solicitação falhou na validação de schema |
| RATE_LIMITED | 429 | Muitas solicitações |
| QUOTA_EXCEEDED | 402 | Limite mensal de renders atingido |
| PAYMENT_REQUIRED | 402 | Limite mensal de registros do ledger Trace / Attest atingido |
| PAYLOAD_TOO_LARGE | 413 | O corpo da solicitação excedeu o limite de 6 MB de JSON |
| RENDER_FAILED | 500 | Falha na geração de PDF |
| INVALID_PDF_URL | 422 | pdfUrl não pôde ser buscado ou não é um PDF válido |
| INVALID_STATUS | 409 | Ação não permitida para o status atual do recurso |
| REMIND_TOO_SOON | 429 | Lembrete já enviado — tente novamente após Retry-After segundos |
| PREVIEW_REQUEST | 410 | Linhas de pré-visualização não podem ser assinadas ou receber lembretes |
| ALREADY_SIGNED | 410 | A solicitação de assinatura já foi assinada |
| ALREADY_VOIDED | 409 | A solicitação de assinatura já foi anulada |
| EXPIRED | 410 | O link de assinatura expirou |
Limites de taxa
Cada endpoint /v1 autenticado é limitado a 20 solicitações/segundo por chave de API e 100 solicitações/segundo por conta, o que vier primeiro. Exceder qualquer um retorna 429 RATE_LIMITED com um cabeçalho Retry-After. Cada resposta — sucesso ou falha — carrega X-RateLimit-Limit, X-RateLimit-Remaining e X-RateLimit-Reset (segundos Unix) para que um cliente possa recuar antes de ser rejeitado. Estes são limites por solicitação e não têm relação com a cota mensal de renders, que retorna 402 QUOTA_EXCEEDED.
14
Servidor MCP (Claude, Cursor, Replit, ChatGPT)
A Kamy expõe um servidor MCP HTTP Streamable em https://mcp.kamy.dev/mcp para que qualquer cliente de IA que fale o Model Context Protocol possa renderizar PDFs, solicitar assinaturas e verificar documentos diretamente de um prompt de chat.
Clientes com configuração JSON (Claude Desktop, Claude Code, Cursor, Continue, Zed)
Adicione ao arquivo de configuração MCP do seu cliente:
{
"mcpServers": {
"kamy": {
"url": "https://mcp.kamy.dev/mcp",
"headers": { "Authorization": "Bearer kamy_pk_..." }
}
}
}
Especificamente para Claude Code: claude mcp add --transport http kamy https://mcp.kamy.dev/mcp --header "Authorization: Bearer $KAMY_API_KEY".
Clientes baseados em formulário (Replit, n8n, Make.com, Zapier, dashboards personalizados)
A maioria das integrações MCP baseadas na web espera três valores no formulário de conexão:
| Field | Value |
|---|---|
Display name / Server name | Kamy (ou qualquer rótulo que você quiser) |
Server URL / Base URL | https://mcp.kamy.dev/mcp (inclua o sufixo /mcp) |
| Cabeçalho personalizado — nome | Authorization (A maiúsculo; esta string exata) |
| Cabeçalho personalizado — valor | Bearer kamy_pk_… (a palavra literal Bearer, depois um espaço, depois sua chave) |
Armadilha comum: colocar a chave de API em um cabeçalho personalizado chamado kamy ou api-key. O servidor MCP só lê a chave de API de Authorization: Bearer kamy_pk_… (ou, como fallback, X-Kamy-Api-Key: kamy_pk_… sem prefixo Bearer). Qualquer outro nome de cabeçalho é ignorado e a conexão falhará com 401.
Verificando se sua chave funciona (curl)
Se o seu cliente exibir um erro confuso como "Session terminated" ou "connection failed", execute isto no seu terminal com a mesma chave de API:
curl -X POST https://mcp.kamy.dev/mcp \
-H "Authorization: Bearer $KAMY_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_signature_requests","arguments":{}}}'
Uma resposta 200 com um bloco JSON-RPC result prova que a chave, o cabeçalho e o servidor MCP da Kamy estão todos saudáveis — qualquer falha que você veja no seu cliente de IA está acontecendo no lado do cliente, não no da Kamy.
Ferramentas disponíveis (59)
Documentos: render_pdf, render_docx, render_pptx, render_xlsx, render_html, convert_document, merge_pdfs, split_pdf, edit_pdf, upload_file, get_upload, render_async, render_batch
Conta, cota e histórico: get_account, list_renders, get_render, get_render_pages, get_job
Agendamento e webhooks: create_schedule, list_schedules, delete_schedule, create_webhook, list_webhooks, test_webhook
Assinaturas: create_signature_request, create_envelope, list_signature_requests, get_signature_certificate, get_signature_request, get_envelope, remind_signature, bulk_signature_requests, preview_field_placement, list_signature_templates, get_signature_template, pki_sign_pdf, verify_pdf_signature
Extração e validação de dados: validate_payload, extract_document, extract_from_render
Confiança e proveniência: attest_artifact, verify_attestation, record_agent_action, get_provenance_chain, trace_record, trace_record_batch, trace_search
Confiança no ecossistema MCP: verify_mcp_server, scan_tool_description
Criação e versionamento de templates: create_template, update_template, publish_template, rollback_template, list_template_versions, get_template_version
Templates e configuração: list_templates, get_template_schema, ask_kamy, get_started
6 delas funcionam anonimamente — descoberta de templates, configuração de SDK e as superfícies públicas de verificação. As demais precisam de uma chave de API com o escopo correspondente; veja a referência MCP para o detalhamento por ferramenta.
15
Opções de render
Cada método de renderização aceita um objeto options para controlar o layout da página, cabeçalhos/rodapés em execução e a segurança do PDF.
await kamy.render({
template: "report",
data,
options: {
format: "letter", // "a4" | "a3" | "letter" | "legal" (default: "a4")
orientation: "landscape", // "portrait" | "landscape" (default: "portrait")
margin: { top: "20mm", right: "15mm", bottom: "20mm", left: "15mm" },
// Running header injected above every page
header: {
html: \`<div style="font-size:9px;color:#999;text-align:right;width:100%">
My Report — page <span class="pageNumber"></span> of <span class="totalPages"></span>
</div>\`,
height: "12mm",
},
// Running footer
footer: {
html: \`<div style="font-size:9px;color:#999;text-align:center;width:100%">
© 2026 Acme Corp — Confidential
</div>\`,
height: "10mm",
},
pageNumbers: true, // auto page numbers via class="pageNumber" in header/footer
// AES-256 encryption
encrypt: {
userPassword: "open-secret", // required to open the PDF
ownerPassword: "owner-secret", // required to change permissions
permissions: {
printing: "highResolution",
copying: false,
modifying: false,
},
},
},
});
| Opção | Tipo | Descrição |
|---|---|---|
| format | "a4" | "a3" | "letter" | "legal" | Tamanho da página (padrão: "a4") |
| orientation | "portrait" | "landscape" | Orientação da página (padrão: "portrait") |
| margin | { top?, right?, bottom?, left? } | Strings de comprimento CSS, ex.: "20mm" ou "0.5in" |
| header | { html, height? } | HTML injetado acima de cada página |
| footer | { html, height? } | HTML injetado abaixo de cada página |
| pageNumbers | boolean | Numeração automática de páginas — use class="pageNumber" no HTML do cabeçalho/rodapé. Modelos do sistema que se beneficiam da numeração de páginas (fatura, orçamento, contrato, relatório, acordo, toda fatura fiscal paga, contrato de locação / carta de oferta dos EAU, certificado salarial, NDA) optam por ativar por padrão; passe false\ aqui para desativar por solicitação. |
| encrypt | EncryptOptions | Proteção por senha AES-256 + flags de permissão |
| validateOnly | boolean | Teste seco: valida modelo + dados sem renderizar ou cobrar um crédito |
| bypassCache | boolean | Ignora o cache de resposta para esta solicitação (força uma nova renderização pelo pool do Chromium). Padrão: false. Consulte "Cache de resposta" abaixo. |
| formFields | FormFieldSpec[] | Adiciona widgets AcroForm preenchíveis ao PDF renderizado (texto, área de texto, caixa de seleção, botão de opção, lista suspensa, assinatura). Até 100 por renderização. Cada especificação é posicionada por página + coordenadas de ponto inferior esquerdo (72 dpi). |
Falhas de pós-processamento. As opções cosméticas — metadata, watermark, formFields, pdfA — degradam em vez de falhar: se uma delas não puder ser aplicada, você ainda recebe seu PDF, com uma entrada POST_PROCESS_FAILED em warnings. encrypt é diferente: é uma garantia de segurança, então uma renderização que solicitou criptografia e não conseguiu produzi-la falha com 500 RENDER_FAILED e nenhum documento é armazenado ou retornado. Você nunca receberá um PDF não criptografado em resposta a uma solicitação encrypt.
Senhas nunca são persistidas. Tanto em /v1/render quanto em /v1/render/async, encrypt.userPassword e encrypt.ownerPassword são usados em memória para criptografar o documento e nunca são gravados no banco de dados. O registro do trabalho assíncrono mantém apenas se cada senha foi fornecida, para que um trabalho na fila possa ser inspecionado sem expor o segredo que desbloqueia sua saída.
validateOnly retorna { ok: true, validated: true, warnings: string[] } sem renderizar — zero créditos consumidos. Avisos são suaves (variáveis não utilizadas, campos obsoletos) e nunca bloqueiam. Erros reais (modelo não encontrado, incompatibilidade de esquema) ainda aparecem exatamente como apareceriam em uma renderização ao vivo.
16
Renderizar a partir de HTML ou URL
Além de modelos nomeados, você pode renderizar strings HTML brutas ou URLs ao vivo. Útil para páginas renderizadas no servidor, documentos pontuais ou fluxos de pré-visualização local que não precisam de um modelo armazenado.
import Kamy from "@kamydev/sdk";
const kamy = new Kamy({ apiKey: process.env.KAMY_API_KEY! });
// Raw HTML string — no template needed
const pdf = await kamy.renderHtml({
html: "<h1>Hello world</h1><p>Generated at runtime.</p>",
options: { format: "a4" },
});
// Live URL — Kamy fetches and renders the page at request time
const pdf2 = await kamy.renderUrl({
url: "https://example.com/reports/2026-q1",
options: { format: "letter", orientation: "landscape" },
});
console.log(pdf.url); // signed URL, same shape as template renders
console.log(pdf2.url);
Ambos os métodos retornam um RenderResponse idêntico e aceitam o mesmo options (format, orientation, margin, encrypt, etc.). Renderizações por URL exigem que a página de destino esteja publicamente acessível no momento da renderização — páginas atrás de autenticação retornarão vazias ou uma tela de login.
17
Cache de resposta
Solicitações de renderização idênticas são deduplicadas automaticamente. Quando você envia um payload cujo HTML compilado e opções de PDF correspondem exatamente a uma renderização bem-sucedida da mesma conta nas últimas 24 horas, o Kamy retorna o PDF idêntico instantaneamente sem reexecutar o navegador. Cota, medição e webhooks disparam de forma idêntica a uma renderização nova — apenas o processamento do Chromium é ignorado.
Uma renderização em cache ainda é uma renderização completa em todos os outros aspectos: ela recebe seu próprio ID de renderização e seu próprio arquivo armazenado, então excluir ou expirar a renderização anterior nunca a afeta. A retenção é contada a partir da data de criação de cada renderização.
O mesmo cache também se aplica a cada item em uma solicitação POST /v1/batch — renderizações repetidas dentro de um lote (ou entre lotes) são deduplicadas individualmente. Ignore o cache para um lote inteiro enviando o cabeçalho X-Kamy-Bypass-Cache: 1 na solicitação do lote.
| Cabeçalho / campo | Direção | Descrição |
|---|---|---|
| X-Kamy-Cache | resposta | hit quando o PDF foi servido do cache, miss caso contrário. |
| options.bypassCache | corpo da solicitação | Defina como true para forçar uma nova renderização. O resultado não é armazenado em cache também, então solicitações idênticas subsequentes também não encontrarão cache. |
| X-Kamy-Bypass-Cache | solicitação | Equivalente em cabeçalho de options.bypassCache. Envie 1 para ignorar. Qualquer uma das formas funciona; o campo do corpo vence se ambos estiverem presentes. |
Escopo do cache: por chave de API (então duas contas renderizando o mesmo modelo nunca compartilham PDFs), janela de 24 horas desde a renderização original, apenas renderizações bem-sucedidas. A chave do cache é um SHA-256 do HTML final compilado (ou URL) mais as opções de PDF resolvidas — então qualquer alteração no modelo, dados, cabeçalho/rodapé, formato, criptografia, etc. produz uma nova chave e uma nova renderização automaticamente. Não há API de invalidação manual; basta enviar uma nova versão do modelo ou alterar os dados.
Quando ignorar: quase nunca. O motivo mais comum é depurar uma renderização que falhou anteriormente e foi corrigida a montante (ex.: uma URL de imagem remota que agora está acessível). Para uso diário, deixe o cache ativado.
Repetições idempotentes são um mecanismo separado do cache de hash de conteúdo acima: uma nova tentativa com o mesmo Idempotency-Key e o mesmo corpo dentro de 24 horas reproduz a resposta original em vez de reexecutar. Repetições carregam Idempotent-Replay: true e X-Idempotency-Hit: true, e o url no corpo é gerado novamente no momento da repetição — links de download armazenados expiram após uma hora, então uma repetição horas depois ainda entrega uma URL funcional. X-Kamy-Bypass-Cache: 1 também ignora a repetição idempotente: a solicitação executa normalmente e sua resposta não é armazenada contra a chave.
18
Versionamento de modelos
Cada kamy push salva uma nova versão imutável. As versões são desacopladas do ponteiro publicado — a versão que suas renderizações realmente usam — para que você possa preparar alterações e fazer a transição sem afetar o tráfego ao vivo.
# After pushing changes, publish the latest draft to make it live
kamy publish <template-id>
# Or pin a specific version number instead of the latest
kamy publish <template-id> --version 4
# Roll the published pointer back to a known-good version
kamy rollback <template-id> 3
# Also overwrite the current draft with that version's HTML at the same time
kamy rollback <template-id> 3 --restore-draft
Cada rota /v1/templates/:id aceita o UUID do modelo ou seu slug — GET, PATCH, DELETE, publish, rollback, versions e versions/:version igualmente. Execute kamy templates para listar ambos. O histórico completo de versões está disponível em GET /api/v1/templates/:id/versions. Consultas por slug em chamadas de renderização sempre resolvem para a versão atualmente publicada.
POST /v1/templates/:id/publish aceita um version opcional e nada mais; o corpo é estrito, então qualquer outra chave retorna um 422 nomeando-a. Anteriormente aceitava um campo notes que era validado e depois descartado — template_versions não tem coluna para armazenar um — então uma rejeição agora é a resposta honesta em vez de um 200 que não registrava nada.
18
Referência da CLI
Instale a CLI globalmente, salve sua chave uma vez, e ela estará disponível para todos os comandos. As chaves são armazenadas em ~/.kamy/config.json e substituídas por KAMY_API_KEY quando a variável de ambiente está definida.
npm i -g @kamydev/cli
kamy config set-key kamy_pk_...
| Comando | Descrição |
|---|---|
| kamy init <slug> | Cria Scaffold.hbs +.css +.schema.json +.sample.json em templates/<slug>/ |
| kamy push <file.hbs> | Faz upsert de um modelo por slug (idempotente, seguro para CI). Aceita --css, --schema, --tag |
| kamy preview <file> | Renderiza um arquivo HTML local e salva o PDF. --watch re-renderiza a cada salvamento, --open abre o visualizador |
| kamy render <template> | Renderiza um modelo nomeado com --data (JSON inline ou @file.json), --output para salvar localmente |
| kamy templates | Lista todos os modelos (sistema + personalizados) na sua conta |
| kamy renders | Lista as 20 renderizações mais recentes com status, tamanho e timestamp |
| kamy publish <template-id> | Publica o rascunho mais recente. --version <n> fixa uma versão específica |
| kamy rollback <template-id> <version> | Reverte o ponteiro publicado. --restore-draft também sobrescreve o rascunho atual |
| kamy uploads create <file> | Envia um arquivo local e imprime sua referência kamy://asset/<id> |
| kamy uploads list | Lista ativos enviados com tamanho e status |
| kamy uploads delete <id> | Exclui um ativo enviado |
| kamy webhooks list | Lista endpoints de webhook |
| kamy webhooks create <url> | Cria um webhook. --events para tipos específicos (padrão: todos). Imprime o segredo uma vez |
| kamy webhooks delete <id> | Exclui um endpoint de webhook |
| kamy webhooks test <id> | Envia um evento de teste. --event <type> (padrão: test.ping) |
| kamy config set-key <api-key> | Salva a chave de API em ~/.kamy/config.json |
| kamy config show | Mostra a configuração atual (origem da chave, URL base da API) |
| kamy doctor | Verificação de saúde: versão do Node, versão da CLI, chave de API, conectividade, informações da conta |
Um loop típico de desenvolvimento local:
# 1. Scaffold a new template
kamy init my-invoice --kind invoice
# → templates/my-invoice/{my-invoice.hbs,.css,.schema.json,.sample.json}
# 2. Preview locally — re-renders on every save, opens in PDF viewer
kamy preview templates/my-invoice/my-invoice.hbs --open --watch
# 3. Push to Kamy (idempotent — safe to run in CI on every commit)
kamy push templates/my-invoice/my-invoice.hbs \
--css templates/my-invoice/my-invoice.css \
--schema templates/my-invoice/my-invoice.schema.json
# 4. Render with the sample data, save locally
kamy render my-invoice \
--data @templates/my-invoice/my-invoice.sample.json \
--output out.pdf
# 5. Verify everything is wired correctly
kamy doctor