Kamy

Kamy renderiza facturas, recibos, contratos y 5 plantillas más de grado de producción con una sola llamada REST o método del SDK de TypeScript. Sin navegador sin cabeza. Sin DevOps.

Documentación

Nuevo — Destacado en Cursor Directory ↗

Genera, firma, demuestra — en 60 segundos.

Instala el SDK, pega tu clave de API, renderiza un PDF, fírmalo con PAdES, comparte la URL de verificación — o envía un PDF por POST y recibe JSON estructurado que tu agente puede demostrar que leyó (ver Kamy Ingest). Toda la capa documental para software nativo de IA, detrás de una API pensada para desarrolladores. Inicio rápido abajo — referencia completa más abajo en la página.

Inicio rápido — copiar y pegar

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

Recibes como respuesta

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

El url es un enlace firmado válido por 1 hora. Ábrelo en tu navegador, o transmite los bytes re-obteniéndolo desde el servidor. ¿Quieres un enlace permanente? Vuelve a emitirlo mediante GET /v1/renders/{id} en cualquier momento.

Lista los renderizados recientes de la cuenta autenticada con GET /v1/renders. Cada entrada tiene un campo status que es uno de "success", "pending" o "failed", además de bytes, durationMs, cost y el templateId / templateName de origen. Soporta ?page y ?pageSize para paginación.

¿Atascado? Cada paso tiene una sección más detallada abajo — o salta a Uso desde backend, REST puro o manejo de errores.

01

Instala el SDK

El SDK de TypeScript funciona en Node.js, Deno, Bun y cualquier runtime de servidor moderno.

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

02

Obtén una clave de API

Inicia sesión en el panel, abre API Keys, haz clic en Nueva clave. Copia la clave una sola vez — solo se muestra en el momento de crearla.

Variable de entorno

KAMY_API_KEY=kamy_pk_...

Mantén la clave solo en tu servidor. Nunca la incluyas en código del lado del cliente.

Ámbitos (scopes). Cada clave lleva una lista de ámbitos que restringen qué operaciones puede realizar. El formulario del panel viene con todos los ámbitos marcados por defecto; desmarca los que no necesites para limitar el impacto si una clave se filtra. Ámbitos disponibles: render, renders:read, templates:read, templates:write, signatures:read, signatures:write, webhooks:read, webhooks:write, schedules:read, schedules:write, uploads:read, uploads:write. Una solicitud que llegue a un endpoint que requiera un ámbito que la clave no tiene devuelve 403 SCOPE_REQUIRED. Las claves emitidas antes de que se lanzara la función de ámbitos tienen una lista vacía y omiten la verificación (compatibilidad hacia atrás).

El ámbito render restringe todos los endpoints que producen 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 y /v1/renders/{id}/split. Retira este ámbito de una clave destinada solo a inspección de solo lectura (renders:read + templates:read) para que una filtración no consuma tu cuota de renderizado.

Los ámbitos de gestión siguen el mismo patrón: schedules:write restringe crear/actualizar/eliminar en /v1/schedules; webhooks:write en /v1/webhooks (incluido el endpoint de entrega de prueba); uploads:write en /v1/uploads (POST + DELETE); templates:write en /v1/templates/{id}, los endpoints de publicar/revertir/versiones y el creador de POST /v1/templates; signatures:write en cada endpoint de firma que muta estado (sobres, solicitudes de firma, recordatorios, plantillas de firma).

Los ámbitos espejo *:read restringen los endpoints GET correspondientes. renders:read cubre la lista/detalle de renderizados y los sub-recursos de extracción/páginas/trabajos; templates:read cubre las lecturas de plantillas y versiones; schedules:read, webhooks:read, uploads:read y signatures:read cubren las superficies de lista/detalle correspondientes. El catálogo público anónimo (GET /v1/templates sin encabezado Authorization) omite los ámbitos por completo — está limitado por IP a nivel de red.

La superficie de procedencia tiene su propio par. attest restringe POST /v1/attest (firmar un hash de artefacto); attestations:read está reservado para lecturas autenticadas de tus propias atestaciones. trace:record además restringe POST /v1/agent-actions y POST /v1/mcp/verify-server (ambos escriben filas en el libro mayor), y trace:read restringe GET /v1/provenance y POST /v1/mcp/scan-tool-description. GET /v1/attest/verify no requiere clave alguna — es la verificación pública orientada al destinatario, limitada por IP y sin datos identificativos. Referencia completa en la página Trace & Attest.

03

Inicio rápido

Renderiza una factura en tres líneas 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 desde un backend Node

Dentro de una ruta de API (Next.js, Hono, Express, Fastify, Nest) — renderiza bajo demanda y devuelve la 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 desde el frontend

Importante: las claves de API nunca deben incrustarse en código de frontend. Llama a tu propio backend, que llama a Kamy. El navegador solo ve la URL firmada 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

Si no estás en un entorno JS, llama al endpoint REST directamente.

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" }
  }'

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

Los encabezados de respuesta incluyen X-Kamy-Cache: hit cuando el PDF se sirvió desde la caché de respuestas (ver Caché de respuestas) y X-Kamy-Cache: miss en caso contrario. Tanto los aciertos como los fallos cuentan contra tu cuota de renderizado y se facturan igual — solo difiere el tiempo de cómputo.

¿Prefieres Postman, Insomnia o Bruno? Importa la colección oficial — postman/kamy.json — cubre las 23 rutas v1 con cuerpos de ejemplo, valores predeterminados de variables de ruta y autenticación Bearer preconfigurada a una única variable de colección {{apiKey}}.

07

Plantillas integradas

Referencia una plantilla por slug. Cada una tiene un esquema de datos totalmente tipado (ver InvoiceData, ReceiptData, etc. exportados desde el SDK). Cada plantilla de sistema registrada es descubrible mediante GET /v1/templates y GET /v1/templates/{slug} sin importar si alguien en tu cuenta ya la ha renderizado — el catálogo se auto-refleja en la primera lectura, con un respaldo por slug para que una sola fila defectuosa en la semilla nunca oculte el resto.

GET /v1/templates también es accesible sin clave de API — los llamadores anónimos reciben el catálogo público (plantillas de sistema más personalizadas marcadas explícitamente como is_public: true), limitado a 30 solicitudes / min / IP. Los llamadores autenticados además ven sus propias plantillas personalizadas y usan los límites estándar por clave / por usuario. Esto hace que el catálogo sea seguro para curl desde una página de documentación o un agente de descubrimiento sin necesidad de registrarse primero.

SlugNombreDescripción
invoiceFacturaPartidas, impuestos, descuentos, condiciones de pago.
receiptReciboCompacto estilo térmico, montos en monoespaciado.
quoteCotizaciónFecha de validez, número de cotización, partidas.
contractContratoSecciones numeradas, bloques de firma.
shipping-labelEtiqueta de envío4×6 pulgadas con código de barras y seguimiento.
certificateCertificadoBorde decorativo, línea de firma.
reportInformePortada, índice, encabezados/pies automáticos.
agreementAcuerdoUna página con partes y términos.
uae-tax-invoiceFactura fiscal EAUBilingüe AR/EN conforme a FTA, IVA 5%.
ksa-zatca-invoiceFactura ZATCA KSASimplificada Fase 1 ZATCA, QR TLV.

08

Plantillas personalizadas Handlebars

Sube tus propias plantillas HTML/Handlebars desde la página Templates — o envíalas directamente desde CI con el CLI kamy push para que tus plantillas de producción permanezcan sincronizadas con tu repositorio en 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: [/* … */] },
});

¿Parchear una plantilla existente? updateTemplate() acepta un UUID o un slug como primer argumento — no necesitas hacer GET-y-luego-PATCH-por-id. Igual 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 de Handlebars disponibles: currency, date, add, number, más todos los integrados (each, if, unless). Valida cargas útiles contra tu esquema en CI sin gastar créditos pasando options.validateOnly: true.

09

Cargas de activos (imágenes grandes, fuentes, logotipos)

Las solicitudes de renderizado están limitadas a 6 MB de cuerpo JSON. Cualquier cosa más grande (fotos de alta resolución, imágenes de folletos multipágina, archivos de fuentes empaquetados) debe subirse una vez mediante createUpload() y luego referenciarse desde tu plantilla por URL — misma llamada de renderizado, fracción de los bytes en la red.

Las cargas usan un patrón de dos pasos: pide a Kamy una URL PUT pre-firmada, transmite el cuerpo del archivo directamente al almacenamiento, luego incrusta el publicUrl devuelto — o el URI abreviado kamy://asset/<id> — en cualquier parte de tus datos de renderizado. La ruta de renderizado resuelve los URI kamy:// a URLs firmadas nuevas automáticamente, así nunca tienes que gestionar la expiración de URLs firmadas tú mismo.

Qué significa expiresAt. La marca de tiempo expiresAt devuelta por POST /v1/uploads aplica solo al uploadUrl pre-firmado — esa URL PUT es de un solo uso y válida por 15 minutos. El activo en sí nunca expira: una vez que PUT tiene éxito y la fila cambia a status: "uploaded", la referencia kamy://asset/<id> se renderiza durante toda la vida del activo (eliminable mediante DELETE /v1/uploads/{id}). Puedes almacenar en caché la referencia kamy:// indefinidamente en tu lado y reutilizarla en tantos renderizados como quieras — cada renderizado genera internamente una URL de descarga firmada nueva de corta duración, así que no necesitas re-subir para mantener las referencias 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()}\` },
});

El límite máximo es 100 MB por objeto. Si una solicitud de renderizado aún alcanza el límite de 6 MB después de cambiar a cargas, recibirás un 413 PAYLOAD_TOO_LARGE estructurado con el límite exacto y una sugerencia de remediación en el cuerpo del error.

09b

Programaciones + integraciones

Entregas recurrentes. Haz POST a /api/v1/schedules para configurar un renderizado impulsado por cron que se ejecute en tu horario y llegue como PDF a tu registro de renderizados, una bandeja de entrada o un chat de WhatsApp. Cada 5 minutos un trabajador recoge las programaciones vencidas, renderiza la plantilla contra el data guardado y lo despacha mediante el 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"
  }'

Canales: email (vía Resend), whatsapp (vía Meta Cloud API — requiere WHATSAPP_PHONE_NUMBER_ID + WHATSAPP_ACCESS_TOKEN en el despliegue) o download (sin entrega; el renderizado llega a tu registro de renderizados para que el panel / API lo obtenga).

Intervalo mínimo. Cada plan puede crear programaciones, pero la frecuencia con la que pueden ejecutarse está limitada: Gratis 60 minutos, Starter 15 minutos, Pro / Business / Scale 5 minutos (el tick del trabajador cron). Una expresión cron que se ejecute más seguido de lo que permite tu plan se rechaza al crearla con 422 VALIDATION_ERROR indicando el intervalo observado. Las programaciones ya creadas no se ven afectadas si los límites cambian.

Marca. Los renderizados programados pasan por la misma fusión automática de kit de marca que las llamadas directas a POST /v1/render — tu logotipo guardado, color de acento, fuente y texto de pie de página se incorporan al PDF renderizado sin configuración adicional. Establece data.brand en la programación para anular campos individuales, o déjalo vacío para usar lo que haya en /dashboard/brand-kit. Las cuentas del plan Gratis también reciben el mismo pie de página Generado por Kamy en la salida programada que aplica POST /v1/render — las programaciones ya no son una vía para evitar la marca en el plan Gratis.

Gestiona mediante GET /api/v1/schedules, PATCH /api/v1/schedules/{id} y DELETE /api/v1/schedules/{id}, o visita /dashboard/schedules para una interfaz con presets de cron, validación de destinatarios y una vista previa en vivo de las próximas ejecuciones. Zapier / Make / n8n. Kamy funciona con cualquier plataforma de automatización sin código que pueda alcanzar un endpoint REST con un token Bearer — no se requiere un conector especial. Configura una acción de webhook personalizada en tu herramienta preferida, apúntala a POST https://kamy.dev/api/v1/render, establece el encabezado Authorization a Bearer YOUR_KAMY_API_KEY, y pasa { template, data } como cuerpo. La respuesta te da una URL de PDF firmada que puedes canalizar al siguiente paso (Slack, Drive, Email, S3 — cualquier cosa que acepte una URL). Combínalo con los webhooks de Kamy (render.completed) para disparar acciones posteriores cuando un render asíncrono termine.

10

Async, lote, bulk y fusión

Para renders de larga duración, trabajos de disparar-y-olvidar, y pipelines de bulk.

// 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 respuesta de lote — cada elemento en results es un objeto de render o una forma { error: { code, message } } (discrimina con "error" in item). El estado HTTP refleja el resultado agregado: 200 todo tuvo éxito, 207 éxito parcial, 502 todo falló. Siempre itera results — nunca asumas que un 2xx significa que cada elemento se renderizó.

Alcance del lote y presupuesto de tiempo. POST /v1/batch impulsa el mismo pipeline que /v1/render y por lo tanto requiere el mismo alcance render en la clave de API. Los elementos se renderizan secuencialmente dentro de un presupuesto de función de 300 segundos; si un lote largo se pasaría de ese límite, los elementos restantes vuelven como entradas { error: { code: "SERVICE_UNAVAILABLE" } } con un 207 en lugar de que toda la solicitud expire. Esos elementos nunca se renderizaron y no se facturan — reintenta solo esos, en un lote más pequeño.

Paridad de salida asíncrona y programada. /v1/render/async y los renders programados pasan por el mismo pipeline de incrustación de activos y marca de agua que /v1/render. Las fuentes remotas <img> y las etiquetas de Google Fonts <link> se incrustan en el servidor antes de que Chromium se ejecute, eliminando la superficie de estancamiento de red que solía hacer que los renders asíncronos + cron fueran más lentos que sus contrapartes síncronas. Las cuentas de nivel gratuito también obtienen el mismo comportamiento de pie de página Generado por Kamy por ciclo en las tres rutas.

Bulk / fusión de correo — para el flujo clásico CSV → muchos PDFs (una plantilla, muchas filas de datos), POST /api/v1/render/bulk devuelve un solo archivo ZIP en lugar de una matriz de URLs. Limitado a 25 filas por llamada para que quepa dentro del presupuesto de tiempo real de la plataforma; para lotes más grandes llama a /v1/batch directamente y divide en fragmentos del lado del 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.

Salida HTML — cuando quieres el mismo pipeline de plantilla + datos canalizado a correo electrónico transaccional (Resend, SendGrid, Mailchimp) en lugar de un PDF, POST /api/v1/render-html omite el pipeline de Chromium y devuelve el HTML renderizado como una cadena. Cuenta como un render contra tu cuota mensual.

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

Generación de XLSX y PPTX

El mismo modelo mental basado en plantillas que PDF, diferente salida. Basado en especificaciones para v1 — pasa un cuerpo JSON estructurado y Kamy emite el archivo. Un render por llamada cuenta contra tu cuota mensual; sin límites por formato.

XLSX — POST /api/v1/render-xlsx con una o más hojas, cada una declarando columnas + filas. Devuelve el libro de trabajo como binario. Los encabezados siempre tienen estilo automático (negrita + relleno gris claro) — no hay una bandera por columna para eso; pasa totalRow para una fila inferior fija (las palabras clave de cadena SUM / AVG / COUNT / MIN / MAX se expanden automáticamente a fórmulas de estilo =SUM(D2:D6); las cadenas con prefijo = pasan textualmente), o numFmt por columna para formato de moneda / porcentaje / fecha.

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 con una matriz de diapositivas, cada una etiquetada con uno de los diseños v1: title, bullets, two-column, table, quote. Pasa theme.accentHex y un theme.fontFace opcional para la marca.

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%"]] }
    ]
  }'

Almacenado como cualquier otro render. Ambas rutas escriben una fila renders y guardan el archivo, por lo que el cargo de cuota tiene un rastro de auditoría y el render aparece en tu registro de renders. El id vuelve en cada respuesta en el encabezado X-Kamy-Render-Id, y agregar ?response=json devuelve el mismo sobre que el resto de la familia render-* — { id, url, bytes, durationMs, format, filename } — en lugar de los bytes crudos. Alimenta ese id a POST /v1/convert cuando el siguiente paso necesite un PDF; fusionar, dividir y firmar solo aceptan PDFs.

10c

Firma electrónica

Envía cualquier PDF renderizado para firma, captura la firma dibujada en un enlace público, y recibe el PDF sellado por correo a ambas partes. Firmas visuales (dibujo en lienzo, no PKI) — mismo peso legal que una firma dibujada a mano en un contrato impreso. Tanto la invitación como la notificación de copia firmada se envían como HTML con marca con el nombre de tu cuenta y el título del documento para que el destinatario vea quién pregunta y qué firma en lugar de un pegado de URL desnudo.

# 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, ... }

Pasa renderId (render existente) o pdfUrl (cualquier PDF públicamente accesible — Kamy lo obtiene y almacena en el servidor). Opcionalmente pasa position: { page, x, y, w, h } en puntos PDF (origen abajo-izquierda) para colocación precisa; el predeterminado es abajo-derecha de la última página. Pasa signOnEveryPage: true para sellar la única firma dibujada del destinatario en cada página del PDF fuente — común para contratos B2B de varias páginas. Pasa requireStamp: true para requerir un sello / timbre de empresa además de la firma personal — el destinatario sube una imagen de sello al momento de firmar y el servidor compone ambos sobre el PDF (flujos de trabajo B2B de UAE, KSA, JP, KR, IN, CN). Obtén una sola solicitud con GET /api/v1/signatures/{id}, lista todas con GET /api/v1/signatures, cancela con PATCH /api/v1/signatures/{id} ({ "action": "void" }), y reenvía la invitación con POST /api/v1/signatures/{id}/remind (limitado a una vez por hora — devuelve HTTP 429 con Retry-After si es demasiado pronto). Pasa reminderCadenceHours en la llamada de creación (24–168) para recordatorios automáticos en un horario — el trabajador reenvía la invitación cada N horas mientras esté pendiente, hasta 3 recordatorios en total. Para transacciones de mayor valor (bienes raíces, empleo, financiero) pasa authMethod: "email_otp" — la página de firma muestra una puerta OTP antes de que el documento cargue, el firmante ingresa un código de 6 dígitos que enviamos por correo a signerEmail, el documento se desbloquea al verificar (usa el mismo canal Resend que la invitación, sin env adicional). OTP por SMS también es compatible vía authMethod: "sms_otp" + signerPhone (E.164) y requiere TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, y TWILIO_PHONE_NUMBER en env — actualmente en espera desde la UI del panel pero activo en la API. Dispara signature.opened en la primera carga de la página de firma y signature.voided al anular. Para flujos de trabajo de equipos de ventas que distribuyen un PDF renderizado a una lista de firmantes (NDAs, MSAs, acuerdos de incorporación), usa POST /api/v1/signatures/bulk con hasta 100 firmantes en una sola solicitud — cada fila produce una fila independiente de signature_requests + correo de invitación y la respuesta devuelve éxito / fallo por fila con HTTP 207 cuando alguna fila falló. El lote se reserva contra tu cuota de firmas atómicamente por adelantado, por lo que un lote que excedería la asignación mensual de firmas de un plan Gratuito se rechaza en su totalidad con QUOTA_EXCEEDED (402) en lugar de despacharse parcialmente. Cada solicitud de firma terminal (firmada, rechazada, delegada, anulada, expirada) expone un Certificado de Finalización PDF — obténlo vía GET /api/v1/signatures/{id}/certificate (autenticación Bearer, alcance signatures.read) o autenticación por token en /api/sign/{token}/certificate. El PDF registra el ciclo de vida completo (invitación → abierto → consentimiento → firmado/rechazado/delegado, con marcas de tiempo, IP, agente de usuario, reconocimiento de consentimiento ESIGN/UETA) y enlaza cruzadamente a la página criptográfica /verify/{sha256} cuando el PDF firmado fue sellado con PAdES. Gestiona desde /dashboard/signatures.

Para PDFs planos (exportaciones de Word, contratos escaneados) que no incluyen widgets AcroForm, adjunta placedFields en la solicitud de creación — la página del firmante muestra entradas rellenables en las coordenadas PDF configuradas (origen abajo-izquierda, puntos), y el servidor sella los valores enviados en la página antes de aplicar la firma. Tipos de campo: text, textarea, checkbox, date, initials, radio, dropdown. Pasa options: ["…"] para radio/desplegable para restringir opciones. Hasta 100 campos por solicitud, los nombres deben ser únicos.

Si un campo lleva sourcePageWidth / sourcePageHeight (el tamaño del visor contra el que mediste las coordenadas), el servidor reescala x/y/w/h al tamaño real de la página. Cuando no puede — el PDF fuente no pudo descargarse o analizarse — las coordenadas se almacenan exactamente como se proporcionaron y la respuesta 201 lleva una entrada warnings diciéndolo. Trata esa advertencia como "verifica la colocación": compruébala con POST /api/v1/signatures/preview-placement, que devuelve los tamaños reales de página y marca los campos que quedan fuera de la página.

Las plantillas pueden llevar su propia configuración de firma predeterminada — establece signature_position, stamp_position, placed_fields, requires_stamp, y sign_on_every_page en la fila de plantilla y cada solicitud de firma creada contra un render de esa plantilla los hereda automáticamente. Te permite colocar una caja de firma una vez en una plantilla NDA y que cada envío la reutilice. Precedencia: cuerpo de solicitud → signatureTemplateId → predeterminados de plantilla → respaldo servidor abajo-derecha.

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 }
    ]
  }'

Agrega una cadena anchor a cualquier campo colocado y el servidor localiza el texto coincidente en el PDF y posiciona el campo allí. Combina anchor con x / y para desplazarte desde la coincidencia. Pasa signatureTemplateId (de POST /api/v1/signature-templates) para aplicar un conjunto predeterminado reutilizable de placedFields, posición, mensaje, expiresIn, y ccEmails — los campos por solicitud siempre anulan los predeterminados de plantilla. La solicitud fusionada se revalida contra el mismo esquema de solicitud (límites, verificaciones de tipo) antes de que el pipeline de firma se ejecute, por lo que una fila de plantilla que precede a una validación más estricta no puede eludir las reglas de entrada actuales.

Para flujos de múltiples firmantes usa POST /api/v1/envelopes en su lugar. Proporciona 2–10 destinatarios; cada uno obtiene un enlace de firma independiente contra el mismo PDF fuente (enrutamiento paralelo). El estado del sobre se convierte en completed cuando el último destinatario firma. Anula todas las solicitudes pendientes en una llamada con PATCH /api/v1/envelopes/{id} ({ "action": "void" }) — dispara signature.voided por destinatario y signature.envelope_completed / signature.envelope_voided a nivel de sobre. La expiración del token predeterminada es de 30 días; pasa expiresIn (segundos, 3 600–2 592 000) para anularla. CC hasta 10 direcciones de observador vía ccEmails — reciben la copia de la invitación y la notificación del PDF firmado. El PDF firmado aterriza en tu registro de renders junto a cualquier otro render.

10d

Sobres de múltiples firmantes

Envía un PDF a 2–10 destinatarios simultáneamente. Cada uno obtiene un enlace de firma independiente; el estado del sobre se convierte en completed cuando el último destinatario firma. Usa routing: "sequential" para condicionar cada invitación detrás del firmante anterior — el destinatario 2 recibe su enlace solo después de que el destinatario 1 complete.

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" }'

Obtén el estado del sobre + todos los destinatarios con GET /api/v1/envelopes/{id}. Lista paginada con GET /api/v1/envelopes. Anular dispara signature.voided por destinatario afectado y signature.envelope_voided a nivel de sobre. La finalización dispara signature.envelope_completed.

10e

Plantillas de firma

Almacena predeterminados reutilizables — placedFields, position, message, expiresIn, ccEmails — y referencia la plantilla por ID en cualquier solicitud de firma. Los campos por solicitud siempre anulan los predeterminados de plantilla.

# 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'"
  }'

Gestiona plantillas con GET /api/v1/signature-templates (lista), GET /api/v1/signature-templates/{id} (detalle), PATCH /api/v1/signature-templates/{id} (actualización), y DELETE /api/v1/signature-templates/{id}.

10f

Sellado criptográfico (PAdES)

Sella un render con un certificado X.509 para que cualquier manipulación rompa la firma. La salida es una firma ETSI EN 319 142-1 PAdES-B-LT: el sello básico (B-B), una marca de tiempo RFC 3161 de una TSA pública incrustada como atributo sin firmar en el SignerInfo (B-T), y una instantánea de revocación CRL incrustada en el PKCS#7 SignedData para que los verificadores puedan verificar la revocación sin conexión (B-LT). Firmado bajo la CA interna de Kamy — los destinatarios verifican la autenticidad en 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, ... }

Pasa withTimestamp: false para omitir el viaje de ida y vuelta de TSA (PAdES-B-B en lugar de B-T) — útil para renders sin conexión o en entornos aislados. El verify_url de la respuesta resuelve a kamy.dev/verify, donde cualquier destinatario puede arrastrar y soltar el PDF firmado — el SHA-256 se calcula en el navegador mediante SubtleCrypto (el archivo nunca se sube), y luego se compara con el registro de signing_events para revelar la identidad del firmante, la cadena de certificados y la marca de tiempo incrustada.

El punto de distribución de CRL en cada certificado hoja resuelve a https://kamy.dev/api/verify/crl — Acrobat y openssl lo siguen automáticamente cuando no hay CRL incrustado. CLI: kamy sign <render-id> envuelve el mismo endpoint; kamy verify <file.pdf> genera el hash de un PDF local e imprime su URL de verificación sin ninguna llamada a la API.

10e

Utilidades de documentos — convertir, dividir, extraer texto y páginas

Cuatro endpoints que funcionan con cualquier render ya existente en tu biblioteca (o aceptan archivos nuevos directamente). GET /api/v1/renders/{id}/extract y el rasterizador de páginas no consumen cuota. POST /api/v1/convert se factura como un render, y POST /api/v1/renders/{id}/split como un render por rango — una división en cuatro rangos cuesta cuatro. Cualquier cosa que produzca un nuevo render en tu biblioteca cuenta como un render; consulta Cuota y uso.

No es lo mismo que Kamy Ingest. Estas utilidades extraen texto o valores de campos AcroForm de un render que Kamy ya ha producido. Para extraer JSON estructurado de un PDF entrante arbitrario (factura de proveedor, formulario de reclamación, contrato recibido) con una URL de verificación pública, consulta Kamy Ingest — endpoint diferente, superpoder diferente.

Convertir DOCX / XLSX / CSV → PDF

Sube un documento de Word, una hoja de cálculo o un CSV y recibe un RenderResult estándar. Utiliza mammoth para la fidelidad de DOCX y SheetJS para las tablas de hojas de cálculo — sin dependencia de LibreOffice por tu parte.

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 rango de páginas

Divide un render en N renders nuevos — uno por rango. Los rangos comienzan en 1 y son inclusivos. Omite to para extender hasta la última página. Hasta 50 rangos por llamada.

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

Extraer texto o campos AcroForm de un render

Funciona contra un render ya existente en tu biblioteca — GET /api/v1/renders/{id}/extract. Endpoint diferente de POST /api/v1/extract (Kamy Ingest, que toma cualquier PDF entrante y devuelve JSON estructurado + URL de verificación).

type=text devuelve bloques de texto por página más una cadena fullText. type=fields devuelve nombres de campos AcroForm, tipos y valores actuales — útil para auditar formularios cumplimentados.

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 a PNG

Cada página se convierte en una URL PNG firmada (caducidad de 1 hora). 150 DPI por defecto para vistas previas en pantalla; pasa dpi=300 para miniaturas de calidad de impresión. Las imágenes se almacenan bajo pdfs/{userId}/pages/{renderId}/ y se sobrescriben en llamadas repetidas.

const { pages } = await kamy.renderPages({ id: "rnd_abc", dpi: 150 });
// pages[0] — { page: 1, width: 1240, height: 1754, url: "https://…" }

10g

Edición de PDF — rellenar, sellar y redactar

Edita un PDF existente antes de enviarlo para firma — rellena campos AcroForm, sella texto en coordenadas exactas, o pinta cajas de redacción opacas sobre contenido sensible. El resultado se guarda como un nuevo render que puedes pasar directamente a POST /api/v1/signatures o POST /api/v1/envelopes — y, al ser un render nuevo, cuenta como uno contra tu cuota mensual. Pasar pdfUrl en lugar de renderId cuesta dos: la fuente obtenida se almacena como un render propio para que la edición tenga algo a lo que referirse.

Proporciona un renderId (un render existente en tu biblioteca) o un pdfUrl (cualquier PDF accesible públicamente — Kamy lo obtiene, edita y almacena). Hasta 50 operaciones por llamada, aplicadas en orden.

Operaciones

  • fill_field — escribe un valor en un widget AcroForm con nombre (texto, casilla de verificación, botón de opción, lista desplegable). Pasa flattenFields: true (por defecto) para incrustar los valores en la página de modo que ya no sean editables.
  • stamp_text — dibuja una cadena de texto en una coordenada del espacio de usuario del PDF (origen abajo a la izquierda). Soporta fontSize, color (hex) y opacity.
  • cover — pinta un rectángulo relleno sobre una región. Solo visual. El texto subyacente permanece en el archivo y aún puede copiarse, pegarse o leerse con cualquier analizador de PDF; la respuesta lleva una advertencia COVER_VISUAL_ONLY por operación. Esta operación se llamaba redact y nunca redactó nada, por lo que se renombró y op: "redact" ahora devuelve 422 REDACTION_NOT_SUPPORTED. Si un valor no debe ser recuperable, mantenlo fuera del documento fuente.
# 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 las coordenadas son puntos del espacio de usuario del PDF (72 pt = 1 pulgada, origen abajo a la izquierda) — el mismo sistema utilizado por placedFields en toda la API de firma electrónica. Usa GET /api/v1/renders/{id}/extract?type=fields para listar los nombres de campos AcroForm en tu PDF fuente antes de rellenar.

Una edición cuesta un render, tanto si pasas renderId como pdfUrl. Una fuente pdfUrl se obtiene, edita en memoria y almacena una vez — no se convierte en un render separado en tu biblioteca.

11

Webhooks

Suscríbete a los eventos render.completed, render.failed, batch.completed y merge.completed. Cada entrega está firmada con HMAC-SHA256 — verifica con verifyWebhook antes de procesar.

// 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

Cuota y uso (programático)

Dos endpoints de solo lectura exponen los límites del plan y el uso en vivo para que puedas conectar paneles, medidores de cuota o comprobaciones previas de CI sin raspar correos de facturación ni adivinar cuándo llegarás al límite.

// 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 endpoints no consumen cuota — no cuentan contra tu presupuesto mensual de renders. me() solo cambia con mejoras de plan, así que almacénalo en caché; usage() es el que debes consultar periódicamente.

usage(), account() y la cuota que la API aplica cuentan lo mismo: renders exitosos dentro del mes calendario UTC actual, sea cual sea su origen — /v1/render, /v1/batch, /v1/merge, /v1/convert, /v1/renders/{id}/split, /v1/pdfs/edit, una firma completada, una programación, un paso de flujo de trabajo o un envío de formulario público. La regla es simple: cualquier cosa que añada un nuevo render a tu biblioteca cuesta uno. Así que usage.renders.remaining es exactamente cuántos renders más puedes hacer antes de un 402 QUOTA_EXCEEDED, no una estimación.

Cada uno de esos endpoints también aplica la cuota, no solo cuenta contra ella: /v1/merge, /v1/pdfs/edit y /v1/renders/{id}/split devuelven 402 QUOTA_EXCEEDED al alcanzar tu límite antes de hacer ningún trabajo. split cuesta un render por rango y reserva el importe completo por adelantado — una división en 4 rangos en un plan con 3 renders restantes se rechaza por completo en lugar de completarse a medias.

En el plan Scale medido, la misma regla decide por qué se te factura: cada render en tu biblioteca reporta una unidad medida, sea cual sea la superficie que lo produjo — la API, una programación, un paso de flujo de trabajo, un envío de formulario público o el Brain. No hay ninguna fuente de renders que cuente contra el uso sin facturarse, ni viceversa.

13

Manejo de errores

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
  }
}
CódigoHTTPDescripción
UNAUTHORIZED401Falta la clave de API o es inválida
INVALID_API_KEY401Formato de clave inválido
API_KEY_REVOKED401La clave fue revocada
FORBIDDEN403Acceso denegado
SCOPE_REQUIRED403La clave de API carece del alcance requerido para esta operación
NOT_FOUND404Plantilla, render o solicitud de firma no encontrados
VALIDATION_ERROR422El cuerpo de la solicitud falló la validación de esquema
RATE_LIMITED429Demasiadas solicitudes
QUOTA_EXCEEDED402Límite mensual de renders alcanzado
PAYMENT_REQUIRED402Límite mensual de registros del ledger Trace / Attest alcanzado
PAYLOAD_TOO_LARGE413El cuerpo de la solicitud superó el límite JSON de 6 MB
RENDER_FAILED500Falló la generación del PDF
INVALID_PDF_URL422No se pudo obtener pdfUrl o no es un PDF válido
INVALID_STATUS409Acción no permitida para el estado actual del recurso
REMIND_TOO_SOON429Recordatorio ya enviado — reintenta después de los segundos de Retry-After
PREVIEW_REQUEST410Las filas de vista previa no pueden firmarse ni recibir recordatorios
ALREADY_SIGNED410La solicitud de firma ya ha sido firmada
ALREADY_VOIDED409La solicitud de firma ya está anulada
EXPIRED410El enlace de firma ha caducado

Límites de velocidad

Cada endpoint /v1 autenticado está limitado a 20 solicitudes/segundo por clave de API y 100 solicitudes/segundo por cuenta, lo que se alcance primero. Superar cualquiera de ellos devuelve 429 RATE_LIMITED con una cabecera Retry-After. Cada respuesta — éxito o error — lleva X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset (segundos Unix) para que un cliente pueda retroceder antes de ser rechazado. Estos son límites por solicitud y no están relacionados con la cuota mensual de renders, que devuelve 402 QUOTA_EXCEEDED.

14

Servidor MCP (Claude, Cursor, Replit, ChatGPT)

Kamy expone un servidor MCP Streamable HTTP en https://mcp.kamy.dev/mcp para que cualquier cliente de IA que hable el Protocolo de Contexto de Modelo pueda renderizar PDFs, solicitar firmas y verificar documentos directamente desde un prompt de chat.

Clientes de configuración JSON (Claude Desktop, Claude Code, Cursor, Continue, Zed)

Colócalo en el archivo de configuración MCP de tu cliente:

{
  "mcpServers": {
    "kamy": {
      "url": "https://mcp.kamy.dev/mcp",
      "headers": { "Authorization": "Bearer kamy_pk_..." }
    }
  }
}

Específicamente para Claude Code: claude mcp add --transport http kamy https://mcp.kamy.dev/mcp --header "Authorization: Bearer $KAMY_API_KEY".

Clientes basados en formularios (Replit, n8n, Make.com, Zapier, paneles personalizados)

La mayoría de las integraciones MCP basadas en web esperan tres valores en su formulario de conexión:

CampoValor
Display name / Server nameKamy (o cualquier etiqueta que quieras)
Server URL / Base URLhttps://mcp.kamy.dev/mcp (incluye el sufijo /mcp)
Cabecera personalizada — nombreAuthorization (A mayúscula; esta cadena exacta)
Cabecera personalizada — valorBearer kamy_pk_… (la palabra literal Bearer, luego un espacio, luego tu clave)

Error común: poner la clave de API bajo una cabecera personalizada llamada kamy o api-key. El servidor MCP solo lee la clave de API de Authorization: Bearer kamy_pk_… (o, como alternativa, X-Kamy-Api-Key: kamy_pk_… sin prefijo Bearer). Cualquier otro nombre de cabecera se ignora y la conexión fallará con un 401.

Verificar que tu clave funciona (curl)

Si tu cliente muestra un error confuso como "Sesión terminada" o "conexión fallida", ejecuta esto desde tu terminal con la misma clave 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":{}}}'

Una respuesta 200 con un bloque JSON-RPC result demuestra que la clave, la cabecera y el servidor MCP de Kamy están todos sanos — cualquier fallo que veas en tu cliente de IA ocurre en el lado del cliente, no en el de Kamy.

Herramientas disponibles (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

Cuenta, cuota e historial: get_account, list_renders, get_render, get_render_pages, get_job

Programación y webhooks: create_schedule, list_schedules, delete_schedule, create_webhook, list_webhooks, test_webhook

Firmas: 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

Extracción y validación de datos: validate_payload, extract_document, extract_from_render

Confianza y procedencia: attest_artifact, verify_attestation, record_agent_action, get_provenance_chain, trace_record, trace_record_batch, trace_search

Confianza del ecosistema MCP: verify_mcp_server, scan_tool_description

Autoría y versionado de plantillas: create_template, update_template, publish_template, rollback_template, list_template_versions, get_template_version

Plantillas y configuración: list_templates, get_template_schema, ask_kamy, get_started

6 de ellas funcionan de forma anónima — descubrimiento de plantillas, configuración del SDK y las superficies públicas de verificación. El resto necesitan una clave de API con el alcance correspondiente; consulta la referencia MCP para el desglose por herramienta.

15

Opciones de render

Cada método de renderizado acepta un objeto options para controlar el diseño de página, encabezados/pies de página y la seguridad del 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,
      },
    },
  },
});
OpciónTipoDescripción
format"a4" | "a3" | "letter" | "legal"Tamaño de página (predeterminado: "a4")
orientation"portrait" | "landscape"Orientación de página (predeterminado: "portrait")
margin{ top?, right?, bottom?, left? }Cadenas de longitud CSS, p. ej. "20mm" o "0.5in"
header{ html, height? }HTML inyectado encima de cada página
footer{ html, height? }HTML inyectado debajo de cada página
pageNumbersbooleanNumeración automática de páginas: usa class="pageNumber" en el HTML de encabezado/pie. Las plantillas del sistema que se benefician de la numeración (factura, cotización, contrato, informe, acuerdo, toda factura de impuestos pagada, contrato de arrendamiento / carta de oferta de EAU, certificado de salario, NDA) se activan por defecto; pasa false\ aquí para desactivarlas por solicitud.
encryptEncryptOptionsProtección con contraseña AES-256 + indicadores de permisos
validateOnlybooleanPrueba en seco: valida plantilla + datos sin renderizar ni cobrar un crédito
bypassCachebooleanOmite la caché de respuestas para esta solicitud (fuerza un renderizado nuevo a través del grupo de Chromium). Predeterminado: false. Consulta "Caché de respuestas" más abajo.
formFieldsFormFieldSpec[]Añade widgets AcroForm rellenables al PDF renderizado (texto, área de texto, casilla de verificación, radio, desplegable, firma). Hasta 100 por renderizado. Cada especificación se posiciona por página + coordenadas de punto inferior izquierdo (72 dpi).

Fallos de posprocesamiento. Las opciones cosméticas — metadata, watermark, formFields, pdfA — se degradan en lugar de fallar: si una de ellas no se puede aplicar, igualmente obtienes tu PDF, con una entrada POST_PROCESS_FAILED en warnings. encrypt es diferente: es una garantía de seguridad, por lo que un renderizado que solicitó cifrado y no pudo producirlo falla con 500 RENDER_FAILED y no se almacena ni devuelve ningún documento. Nunca recibirás un PDF sin cifrar en respuesta a una solicitud encrypt.

Las contraseñas nunca se persisten. Tanto en /v1/render como en /v1/render/async, encrypt.userPassword y encrypt.ownerPassword se usan en memoria para cifrar el documento y nunca se escriben en la base de datos. El registro del trabajo asíncrono conserva solo si se proporcionó cada contraseña, de modo que un trabajo en cola se puede inspeccionar sin exponer el secreto que desbloquea su salida.

validateOnly devuelve { ok: true, validated: true, warnings: string[] } sin renderizar — cero créditos consumidos. Las advertencias son suaves (variables no utilizadas, campos obsoletos) y nunca bloquean. Los errores reales (plantilla no encontrada, discrepancia de esquema) siguen apareciendo exactamente como lo harían en un renderizado en vivo.

16

Renderizar desde HTML o una URL

Además de las plantillas con nombre, puedes renderizar cadenas HTML sin procesar o URLs en vivo. Útil para páginas renderizadas en el servidor, documentos puntuales o flujos de vista previa local que no necesitan una plantilla almacenada.

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 métodos devuelven un RenderResponse idéntico y aceptan el mismo options (format, orientation, margin, encrypt, etc.). Los renderizados por URL requieren que la página de destino sea accesible públicamente en el momento del renderizado — las páginas detrás de autenticación devolverán vacío o una pantalla de inicio de sesión.

17

Caché de respuestas

Las solicitudes de renderizado idénticas se deduplican automáticamente. Cuando envías un payload cuyo HTML compilado y opciones de PDF coinciden exactamente con un renderizado exitoso de la misma cuenta en las últimas 24 horas, Kamy devuelve el PDF idéntico al instante sin volver a ejecutar el navegador. La cuota, la medición y los webhooks se activan de forma idéntica a un renderizado nuevo — solo se omite el cómputo de Chromium.

Un renderizado en caché sigue siendo un renderizado completo en todos los demás aspectos: obtiene su propio ID de renderizado y su propio archivo almacenado, por lo que eliminar o expirar el renderizado anterior nunca lo afecta. La retención se cuenta desde la fecha de creación de cada renderizado.

La misma caché también se aplica a cada elemento de una solicitud POST /v1/batch — los renderizados repetidos dentro de un lote (o entre lotes) se deduplican individualmente. Omite la caché para un lote completo enviando el encabezado X-Kamy-Bypass-Cache: 1 en la solicitud del lote.

Encabezado / campoDirecciónDescripción
X-Kamy-Cacherespuestahit cuando el PDF se sirvió desde la caché, miss en caso contrario.
options.bypassCachecuerpo de solicitudEstablecer en true para forzar un renderizado nuevo. El resultado tampoco se almacena en caché, por lo que las solicitudes idénticas posteriores también fallarán.
X-Kamy-Bypass-CachesolicitudEquivalente de encabezado de options.bypassCache. Envía 1 para omitir. Cualquiera de las dos formas funciona; el campo del cuerpo gana si ambos están presentes.

Alcance de la caché: por clave de API (por lo que dos cuentas que renderizan la misma plantilla nunca comparten PDFs), ventana de 24 horas desde el renderizado original, solo renderizados exitosos. La clave de caché es un SHA-256 del HTML final compilado (o URL) más las opciones de PDF resueltas — por lo que cualquier cambio en la plantilla, datos, encabezado/pie, formato, cifrado, etc. produce una nueva clave y un renderizado nuevo automáticamente. No hay API de invalidación manual; simplemente publica una nueva versión de plantilla o cambia los datos.

Cuándo omitir: casi nunca. La razón más común es depurar un renderizado que falló previamente y que desde entonces se ha corregido aguas arriba (p. ej., una URL de imagen remota que ahora es accesible). Para uso diario, deja la caché activada.

Las repeticiones idempotentes son un mecanismo separado de la caché de hash de contenido anterior: un reintento con el mismo Idempotency-Key y el mismo cuerpo dentro de 24 horas reproduce la respuesta original en lugar de re-ejecutar. Las repeticiones llevan Idempotent-Replay: true y X-Idempotency-Hit: true, y el url en el cuerpo se acuña nuevo en el momento de la repetición — los enlaces de descarga almacenados expiran después de una hora, por lo que una repetición horas después aún te entrega una URL funcional. X-Kamy-Bypass-Cache: 1 también omite la repetición idempotente: la solicitud se ejecuta normalmente y su respuesta no se almacena contra la clave.

18

Versionado de plantillas

Cada kamy push guarda una nueva versión inmutable. Las versiones están desacopladas del puntero publicado — la versión que tus renderizados realmente usan — para que puedas preparar cambios y hacer la transición sin afectar el tráfico en 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 ruta /v1/templates/:id acepta el UUID de la plantilla o su slug — GET, PATCH, DELETE, publish, rollback, versions y versions/:version por igual. Ejecuta kamy templates para listar ambos. El historial completo de versiones está disponible en GET /api/v1/templates/:id/versions. Las búsquedas por slug en las llamadas de renderizado siempre resuelven a la versión actualmente publicada.

POST /v1/templates/:id/publish acepta un version opcional y nada más; el cuerpo es estricto, por lo que cualquier otra clave devuelve un 422 que la nombra. Anteriormente aceptaba un campo notes que se validaba y luego se descartaba — template_versions no tiene columna para almacenar uno — por lo que un rechazo es ahora la respuesta honesta en lugar de un 200 que no registraba nada.

18

Referencia de CLI

Instala la CLI globalmente, guarda tu clave una vez, y estará disponible para cada comando. Las claves se almacenan en ~/.kamy/config.json y se anulan con KAMY_API_KEY cuando la variable de entorno está configurada.

npm i -g @kamydev/cli
kamy config set-key kamy_pk_...
ComandoDescripción
kamy init <slug>Crea Scaffold.hbs +.css +.schema.json +.sample.json en templates/<slug>/
kamy push <file.hbs>Inserta o actualiza una plantilla por slug (idempotente, seguro para CI). Acepta --css, --schema, --tag
kamy preview <file>Renderiza un archivo HTML local y guarda el PDF. --watch re-renderiza en cada guardado, --open abre el visor
kamy render <template>Renderiza una plantilla con nombre con --data (JSON en línea o @archivo.json), --output para guardar localmente
kamy templatesLista todas las plantillas (sistema + personalizadas) en tu cuenta
kamy rendersLista los 20 renderizados más recientes con estado, tamaño y marca de tiempo
kamy publish <template-id>Publica el último borrador. --version <n> fija una versión específica
kamy rollback <template-id> <version>Revierte el puntero publicado. --restore-draft también sobrescribe el borrador actual
kamy uploads create <file>Sube un archivo local e imprime su referencia kamy://asset/<id>
kamy uploads listLista los activos subidos con tamaño y estado
kamy uploads delete <id>Elimina un activo subido
kamy webhooks listLista los endpoints de webhook
kamy webhooks create <url>Crea un webhook. --events para tipos específicos (predeterminado: todos). Imprime el secreto una vez
kamy webhooks delete <id>Elimina un endpoint de webhook
kamy webhooks test <id>Envía un evento de prueba. --event <type> (predeterminado: test.ping)
kamy config set-key <api-key>Guarda la clave de API en ~/.kamy/config.json
kamy config showMuestra la configuración actual (fuente de clave, URL base de API)
kamy doctorVerificación de salud: versión de Node, versión de CLI, clave de API, conectividad, información de cuenta

Un bucle típico de desarrollo 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