Guía rápida de la API

URL base https://api.pressfox.io. JSON de entrada, JSON de salida. Los ids públicos llevan prefijo: ws_ espacio de trabajo, tpl_ plantilla, ver_ versión de plantilla, rpt_ informe, brd_ perfil de marca, ast_ archivo, key_ clave de API, evt_ evento de webhook.

En esta página

Guía rápida

  1. Crea una clave. En la app, abre Settings → API keys y crea una clave de prueba. Empieza con pf_test_, se muestra una sola vez y genera informes con marca de agua que no se miden y se guardan 24 horas. Crea una clave real (pf_live_) cuando estés listo para enviar documentos reales.
  2. Publica una plantilla. Copia un paquete o diseña la tuya, vincula los campos a rutas de tus datos de ejemplo y publica. Publicar crea una versión inmutable y genera su contrato de datos. Copia la clave de la plantilla desde el diálogo Publish, o lista las plantillas con GET /api/v1/templates.
  3. Envía datos. POST /api/v1/reports con el id de la plantilla y tus datos. La respuesta trae el enlace alojado y el PDF. Esa es toda la integración.
Solicitud
POST /api/v1/reports
Authorization: Bearer pf_live_7f3a…
Idempotency-Key: visit-48213
Content-Type: application/json

{
  "template": "clinic-visit-summary",
  "payload": { "patient": { "name": "A. Rivera" }, "results": [ { "test": "HbA1c", "value": 6.1 } ] },
  "brand": "tolland",
  "access": { "kind": "password", "password": "correct-horse-battery" }
}
Respuesta cuando el render terminó dentro de la solicitud200
HTTP/1.1 200 OK
Content-Type: application/json

{
  "report_id": "rpt_01j9xk7q4",
  "title": "Clinic visit summary – 2026-10-07",
  "template_id": "tpl_01j9xk2m8",
  "template_key": "clinic-visit-summary",
  "template_version": 4,
  "status": "ready",
  "mode": "live",
  "language": "en",
  "url": "https://app.pressfox.io/report/k7Qm2Zb9pX4nT1vW8sLq0A",
  "pdf_url": "https://app.pressfox.io/report/k7Qm2Zb9pX4nT1vW8sLq0A/pdf",
  "markdown_url": null,
  "pages": 2,
  "units": 1,
  "created_at": "2026-10-07T09:14:02.118402+00:00",
  "ready_at": "2026-10-07T09:14:03.402911+00:00",
  "error": null,
  "message": "Filed. Don't lose the link."
}

Autenticación

Envía Authorization: Bearer pf_live_… o pf_test_… en cada solicitud. Una clave pertenece a un espacio de trabajo y actúa por todo el espacio de trabajo; no lleva usuario ni rol. Revócala desde la app cuando quieras. Las solicitudes sin una clave válida reciben 401 unauthorized. GET /api/v1/whoami devuelve { workspace_id, workspace, mode, key } de la clave que enviaste, una forma barata de revisar un despliegue.

Idempotencia. Envía un encabezado Idempotency-Key en POST /api/v1/reports: cualquier texto de hasta 128 caracteres, único dentro de tu espacio de trabajo. Usa tu propio id de registro, como un número de factura o de consulta. Durante 24 horas, la misma clave devuelve el estado y el cuerpo originales. La misma clave con otro cuerpo, o con el otro modo de clave, recibe 409 conflict (That Idempotency-Key was already used with a different request). Una segunda solicitud mientras la primera sigue en curso también recibe 409 (still in flight). Una solicitud rechazada libera su clave, así puedes reintentar con ella una vez corregidos los datos. Las claves de más de 128 caracteres se rechazan con 422 invalid_value.

Informes

POST /api/v1/reports

Crea un informe a partir de una versión publicada de la plantilla y tus datos: la versión que fijes ("template": "clinic-visit-summary@3", ver versiones y fijación), o la actual si no indicas ninguna. Los datos se validan contra el contrato de esa versión antes de encolar nada; unos datos rechazados nunca se convierten en informe. Después la solicitud espera hasta 2.5 segundos el render. Devuelve 200 con el objeto completo cuando el render terminó a tiempo, 202 con status: "queued" o "rendering" cuando no, y 500 con status: "failed" y el motivo en error cuando el render falló durante la espera. Los datos de más de 1 MiB, o estimados en más de 50 páginas según el ejemplo de la versión, van a una cola aparte con un plazo de render más largo (120 segundos en lugar de 20); igual pueden responder 200 si terminan rápido.

CampoTipoNotas
templatestring, obligatorioUna plantilla de este espacio de trabajo, por clave (clinic-visit-summary) o por id (tpl_…). La clave se define en la app y no cambia al renombrar la plantilla; si la cambias allí, la clave anterior deja de funcionar, mientras que el id nunca cambia. Las plantillas desconocidas o archivadas reciben 404 not_found; una plantilla sin versión publicada recibe 422 template_not_published. Agrega @N para fijar la versión N (clinic-visit-summary@3, tpl_…@3); sin eso el informe usa la versión actual. Una versión desconocida recibe 404 version_not_found, una retirada 410 version_retired, y un sufijo mal formado (key@, key@0, key@x, key@1@2) 422 invalid_value. template_id, el nombre original de este campo, acepta los mismos valores; envía uno u otro, no ambos.
template_versionintegerLa versión a generar, un entero positivo: lo mismo que @N en template, para clientes que guardan el número aparte. Enviar ambos con números distintos recibe 422 invalid_value en $.template_version; el mismo número dos veces está bien. Omite ambos para usar la versión actual.
payloadcualquier JSON, obligatorioTiene que cumplir el contrato de la versión. Hasta 8 MiB (8,388,608 bytes) como JSON canónico; más grande recibe 413 payload_too_large. Las imágenes se envían como enlaces: una URL https (una prefirmada sirve) o asset:<id>; ver imágenes.
optionsobjectValores para los interruptores que declara la versión de la plantilla, por nombre. Una clave que la versión no declara recibe 422 unknown_option, y el mensaje lista los nombres que sí declara.
brandstringUna marca de la app, por clave (tolland) o por id (brd_…): el informe se genera con sus colores, fuente y logo. Omítela para generarlo con el tema propio de la versión. Lista las claves con GET /api/v1/brands. Una desconocida recibe 404 brand_not_found, que indica el valor. brand_id es el campo original y acepta los mismos valores; enviar ambos recibe 422 invalid_value.
accessobjectkind más su campo; ver políticas de acceso. Por defecto { "kind": "link" }.
pdf_passwordstringUna contraseña en el propio PDF: el archivo solo se abre con ella. Al menos 4 caracteres y como máximo 127 bytes, o "same_as_link" para usar la access.password de un enlace password. Standard o superior; en planes inferiores, 402 pdf_password_not_included. Sin ella, una plantilla configurada para tomar la contraseña de un campo de los datos usa ese campo. Nunca se guarda con el informe ni se devuelve; ver una contraseña en el PDF.
languagestringEl idioma del informe: en, es, pt, de o fr. Por defecto, el idioma en que está escrita la plantilla. Cualquier otro necesita las traducciones de la plantilla a ese idioma, o recibe 422 language_not_available con la lista de los que tiene; ver idiomas.
translate_fromstringEl idioma en que está escrito el texto libre de tus datos, cuando difiere de language: los campos que la plantilla marca como Translate se traducen automáticamente al idioma del informe antes de generarlo, y el informe lo indica. Todos los planes de pago, desde Starter; ver idiomas.
formatsarray of stringsSalidas extra: ["markdown"] agrega una versión en Markdown del informe (ver salida en Markdown). El HTML y el PDF se generan siempre; los valores son "html", "pdf" y "markdown", y cualquier otro recibe 422 unknown_format. Omítelo para seguir el ajuste del espacio de trabajo Always include Markdown; [] excluye a un informe.
titlestringHasta 200 caracteres, una línea. Nombra el informe en la app y en los correos de envío, y nombra sus archivos: el PDF se descarga como <title>.pdf. Omítelo para usar el patrón de título de la plantilla, por defecto el nombre de la plantilla y el día (Clinic visit summary – 2026-10-07). Los títulos se guardan sin cifrar, a diferencia de los datos: no incluyas datos personales en ellos.
retain_daysintegerGuarda este informe ese número de días (de 1 a 3,650) y luego lo borra, con su página, su PDF y sus enlaces. Como máximo la retención de informes del espacio de trabajo, si un propietario fijó una; por encima recibe 422 retain_days_too_long. Sin él, el informe se guarda como lo define el espacio de trabajo: por defecto, mientras la suscripción esté activa. Ver cuánto tiempo se guardan los informes.

Objeto de respuesta, el mismo en todos los endpoints de informes:

CampoTipoNotas
report_idstringrpt_…
titlestringEl título del informe, fijado al crearlo: el tuyo, o el patrón de la plantilla llenado con los datos.
template_id, template_keystringLa plantilla de la que salió el informe; la clave tal como está ahora.
template_versionintegerEl número de versión con que se generó el informe, fijada o actual en ese momento. Nunca cambia para un informe.
statusstring"queued", "rendering", "ready", "failed" o "deleted"
modestring"live" o "test", según la clave
languagestringEl idioma del informe, fijado al crearlo: el tuyo, o el idioma original de la plantilla.
urlstringLa página alojada, …/report/<token>. Presente desde la creación; la página responde según el estado del informe hasta que está listo. El token es aleatorio e independiente del id.
pdf_urlstringurl más /pdf. Presente desde la creación; el archivo existe cuando el informe está listo. Se abre en el navegador; agrega ?download=1 para descargarlo. En ambos casos el archivo lleva el título como nombre.
markdown_urlstring or nullurl más /md: el informe en Markdown, cuando se pidió (formats o el ajuste del espacio de trabajo); null si no. Misma política de acceso que el PDF; ver salida en Markdown.
pdf_protectedbooleantrue cuando el PDF solo se abre con una contraseña. La contraseña nunca se devuelve.
pages, unitsintegerCantidad de páginas y las unidades con que se mide, ceil(pages / 50). 0 hasta que está listo.
created_at, ready_atRFC 3339 or nullUTC con microsegundos y desfase, como en los ejemplos. ready_at es null hasta que está listo.
retain_untilRFC 3339 o nullCuándo terminan el informe, sus enlaces y su PDF. null: sin fin mientras la suscripción del espacio de trabajo esté activa. Ver cuánto tiempo se guardan los informes.
errorstring or nullLo que rechazó el plano de render, cuando status es "failed". Null en otro caso.
messagestringUn mensaje de una línea para el registro, en inglés. No es para procesarlo.
Respuesta cuando el render sigue en curso202
HTTP/1.1 202 Accepted

{
  "report_id": "rpt_01j9xkb2w",
  "title": "Clinic visit summary – 2026-10-07",
  "template_id": "tpl_01j9xk2m8",
  "template_key": "clinic-visit-summary",
  "template_version": 4,
  "status": "rendering",
  "mode": "live",
  "language": "en",
  "url": "https://app.pressfox.io/report/Qh3Lm8Tz2dR5yB7cN9kE1F",
  "pdf_url": "https://app.pressfox.io/report/Qh3Lm8Tz2dR5yB7cN9kE1F/pdf",
  "markdown_url": null,
  "pages": 0,
  "units": 0,
  "created_at": "2026-10-07T09:20:41.007310+00:00",
  "ready_at": null,
  "error": null,
  "message": "Rendering. Poll this report or wait for the webhook."
}
Respuesta cuando el render falló durante la espera500
HTTP/1.1 500 Internal Server Error

{
  "report_id": "rpt_01j9xkc9f",
  "title": "Clinic visit summary – 2026-10-07",
  "template_id": "tpl_01j9xk2m8",
  "template_key": "clinic-visit-summary",
  "template_version": 4,
  "status": "failed",
  "mode": "live",
  "language": "en",
  "url": "https://app.pressfox.io/report/Vb8Nq1Rw5kJ2hG6mP3xD4S",
  "pdf_url": "https://app.pressfox.io/report/Vb8Nq1Rw5kJ2hG6mP3xD4S/pdf",
  "markdown_url": null,
  "pages": 0,
  "units": 0,
  "created_at": "2026-10-07T09:22:10.551004+00:00",
  "ready_at": null,
  "error": "too-many-pages: document lays out to about 7 pages; the cap is 3",
  "message": "The render failed. Fix what the error names and send it again."
}

Un informe fallido es un informe: tiene un id, aparece en las listas y su error indica la causa. Un render que falla después de que la solicitud respondió se informa con GET, con el webhook report.failed, o con ambos.

GET /api/v1/reports/{id}

Devuelve el objeto del informe. Consúltalo después de un 202 con un intervalo razonable, o mejor usa un webhook. Una clave de prueba ve solo informes de prueba, una clave real solo los reales; los ids del otro modo responden 404.

GET /api/v1/reports?template=&status=&cursor=&limit=

Lista los informes del modo de la clave, del más nuevo al más viejo, como { items, next_cursor }. Todos los filtros son opcionales: template (una clave o id de plantilla, o key@N para los informes hechos con la versión N), status (uno de los cinco estados; sin él se omiten los informes borrados), limit (de 1 a 200, por defecto 50) y cursor, el next_cursor de la página anterior. next_cursor es null en la última página. Un cursor que esta API no emitió recibe 422 invalid_value.

GET /api/v1/reports?template=clinic-visit-summary&status=ready&limit=50

{
  "items": [ { "report_id": "rpt_01j9xk7q4", "template_key": "clinic-visit-summary", "template_version": 4, "status": "ready", "mode": "live", "language": "en", "pages": 2, "units": 1, … } ],
  "next_cursor": "MjAyNi0xMC0wN1QwOToxNDowMi4xMTg0MDIrMDA6MDB8MDE5OWI…"
}

DELETE /api/v1/reports/{id}

Marca el informe como borrado y quita del almacenamiento los datos, el HTML, el PDF y el Markdown; el enlace deja de responder. Devuelve 204, también si el informe ya estaba borrado. La fila queda, sin clave, así GET sigue respondiendo con status: "deleted". Las unidades medidas no se reembolsan.

Políticas de acceso

access.kind decide quién puede abrir el enlace alojado. La política se define al crear el informe y no se puede editar por esta API; la app puede revocar un enlace, lo que emite un token nuevo.

TipoCamposPlanesLa página alojada
linkningunotodosCualquiera con el enlace. Es el valor por defecto.
passwordpassword, al menos 8 caracteresStandard o superiorPide la contraseña (401) y crea una cookie por 12 horas. Cinco intentos fallidos en 15 minutos cierran la puerta con 429 y una espera creciente.
expiryexpires_at, una fecha ISO 8601 futuraStarter o superiorResponde 410 una vez pasada la fecha.
signedningunoPro o superiorAbre solo con ?exp=<unix seconds>&sig=<hex HMAC-SHA256 over "token.exp"> generado con la clave de firma del espacio de trabajo; si no, 403. Los url y pdf_url del cuerpo del informe ya llevan una firma válida por siete días; POST /api/v1/reports/{id}/links con {"expires_in": seconds} (de 60 segundos a 30 días) genera nuevos y responde {url, pdf_url, markdown_url, expires_at}.
privateningunoPro o superiorAbre solo para un miembro del espacio de trabajo con sesión iniciada; los demás ven 404.

Un tipo fuera de esta lista recibe 422 invalid_value en $.access.kind. Si falta password o expires_at, se recibe 422 missing_field. expiry en Free, o password en Free o Starter, recibe 402 access_policy_not_included; signed o private por debajo de Pro recibe 402 signed_urls_not_included.

Una contraseña en el propio PDF

Una política de acceso decide quién abre el enlace alojado; una contraseña en el PDF acompaña al archivo a donde vaya: como adjunto, reenviado o descargado. El PDF se cifra con AES-256 y se abre en cualquier visor habitual cuando el lector escribe la contraseña. El lector puede seguir imprimiendo, copiando y usando un lector de pantalla: la contraseña deja fuera a extraños, no restringe al lector. Standard o superior.

  • Por informe: envía "pdf_password": "N0rthwind-1984", o "same_as_link" con access.kind password para usar la contraseña del enlace también en el archivo.
  • Desde los datos: en el diseñador, PDF password indica un campo de los datos y un formato: As it is (tal cual), Digits only (solo dígitos: 1984-03-07 pasa a 19840307) o Last 4 (los últimos 4, sin espacios). El ajuste queda fijo con la versión; cada informe necesita ese campo, o recibe 422 invalid_value que lo nombra. Un pdf_password en la solicitud gana sobre el ajuste.

Receta: estados de cuenta que se abren con la fecha de nacimiento del cliente. Agrega client.birth_date a los datos, configura PDF password con ese campo y Digits only, y publica. Explica una vez a tus clientes cómo saber su contraseña; el botón de descarga de la página alojada indica que el PDF la necesita, sin decirla.

La contraseña se usa para cifrar el archivo y se borra con los datos enviados cuando el informe está listo: Pressfox no puede recuperarla, y no aparece en ninguna respuesta, registro ni webhook. Un informe cuyo PDF no se pudo cifrar falla en lugar de salir sin protección.

Compartir por correo

POST /api/v1/reports/{id}/shares con {"emails": [...], "duration": "24h" | "48h" | "7d" | "30d" | "none", "message"?: "..."} envía por correo un informe listo a hasta 50 personas, un correo para cada una, desde Pressfox en nombre de tu espacio de trabajo. Cada persona recibe su propio enlace a la página alojada y al PDF: /report/<token>?share=<rcp_id>&exp=<unix seconds, 0 for none>&ssig=<hex>, un HMAC-SHA256 con la clave de firma del espacio de trabajo sobre "token.share.<rcp_id>.<exp>". No funciona como enlace firmado ni como enlace de QR, y ellos no funcionan como este.

Un enlace compartido abre ese informe con cualquier tipo de acceso, incluidos contraseña y privado: a la persona se la eligió por su dirección. Responde 410 cuando vence su plazo, cuando vence el propio informe y cuando el envío o ese destinatario se revoca en la app; revocar el enlace del informe o borrar el informe también lo cierra (404). "none" sigue limitado por el vencimiento del propio informe y la retención del plan; el expires_at de la respuesta dice cuándo dejan de funcionar los enlaces. Las direcciones se recortan, se pasan a minúsculas y se eliminan duplicados; una dirección inválida rechaza toda la solicitud con 422 invalid_email, indicándola. Un espacio de trabajo comparte con hasta 25 personas al día en Free, 100 en Starter y 500 desde Standard (429 share_limit con Retry-After). La respuesta es 201 con el envío y sus destinatarios; las aperturas se cuentan por destinatario en la app, nunca de bots ni de vistas previas de enlaces, y el correo no lleva píxel de rastreo.

Idiomas, traducción y resúmenes

Un informe está en uno de cinco idiomas: inglés (en), español (es), portugués (pt), alemán (de) y francés (fr). Tres cosas lo siguen.

  • El texto de la plantilla. Una plantilla se escribe en un idioma (su source_language, inglés salvo que el autor lo cambie) y puede llevar traducciones a los demás, con borradores en el editor, revisadas por su autor y fijadas en cada versión publicada. "language": "es" genera las etiquetas, títulos y pies en español; no interviene ningún modelo. Un idioma para el que la versión no tiene traducciones recibe 422 language_not_available, que lista los que tiene; GET /api/v1/templates también los lista. Una etiqueta sin traducción se imprime en el idioma original.
  • Formato y palabras fijas. Los números y las fechas usan la configuración regional del idioma (es-ES, pt-BR, de-DE, fr-FR) salvo que la plantilla defina la suya; las palabras que imprime Pressfox por su cuenta (números de página, el anexo de fotos, los botones y páginas de estado de la página alojada) están en el idioma del informe.
  • Tu texto, cuando lo pides. Los valores de los datos se imprimen tal como los envías. Para traducir texto libre escrito en otro idioma, envía "translate_from": "pt": los campos que el autor de la plantilla marcó como Translate (en el contrato, "x-pressfox-translate": true) se traducen automáticamente al idioma del informe cuando empieza el render. Los ids, códigos, números y fechas nunca cambian, y cada número del original tiene que sobrevivir a la traducción o no se usa nada. El PDF y la página alojada llevan un aviso en el idioma del informe, como Campos de texto traducidos automáticamente del portugués.

Traducir texto: las reglas. translate_from igual a language no hace nada. Una plantilla que no marca ningún campo recibe 422 nothing_to_translate; más de 5,000 textos distintos o 400,000 caracteres para traducir, 422 invalid_value en $.payload. Requiere un plan con unidades traducidas: sin ellas, 402 translated_units_per_month_not_included; pasadas las unidades del mes, 402 translated_units_limit. Un informe se cuenta por sus unidades estimadas, una vez, por más que se reintente su render. Un espacio de trabajo cuya industria es salud recibe 403 ai_off. Una traducción que no se puede completar de forma confiable hace fallar el informe, con un error que indica la causa y sin texto de los datos; envíalo de nuevo, o sin translate_from para conservar el texto original.

Resúmenes con IA. Una plantilla puede tener un widget AI summary que lee los campos que eligió su autor (por ejemplo la descripción de cada hallazgo) e imprime unas frases sobre ellos, identificadas como Escrito por IA, en el idioma del informe y después de cualquier traducción. Se escribe una vez cuando empieza el render y queda fijado con el informe. Requiere un plan que incluya resúmenes (todos los planes de pago, desde Starter); si no, en un espacio de salud, cuando los campos están vacíos o cuando la respuesta del modelo no sirve, el widget no imprime nada y el informe igual se genera. No envías nada extra para esto.

Las llamadas al modelo ven solo los campos marcados de ese informe, nunca imágenes, y no se guarda nada: los datos se borran cuando el informe está listo, como siempre.

Salida en Markdown

Cada informe también se puede leer en Markdown: el mismo contenido que su HTML y su PDF, en orden de lectura, con títulos, tablas y etiquetas intactos, para que personas y herramientas de IA (asistentes, búsqueda, pipelines RAG, agentes por MCP) lo lean sin procesar un PDF. Lo genera código a partir del documento del informe al momento del render, sin IA: el mismo informe siempre da los mismos bytes. El PDF sigue siendo el documento oficial. Está disponible en todos los planes y no cambia las unidades.

Pedirlo. Envía "formats": ["markdown"] en POST /api/v1/reports. El HTML y el PDF se generan siempre; formats agrega salidas y acepta "html", "pdf" y "markdown". Cualquier otro valor recibe 422 unknown_format en $.formats[i]. El ajuste del espacio de trabajo Always include Markdown (Settings → Reports en la app) lo activa para cada informe; el campo de la solicitud manda cuando se envía, así "formats": [] deja a un informe sin él. Un informe hecho sin él tiene markdown_url: null y nunca lo obtiene después: sus insumos se borran cuando está listo.

Solicitud
POST /api/v1/reports
Authorization: Bearer pf_test_7f3a…
Content-Type: application/json

{ "template": "vessel-inspection", "payload": { … }, "formats": ["markdown"] }

Obtenerlo. markdown_url en el objeto del informe, en el webhook report.ready y en POST /links es el archivo alojado, …/report/<token>/md. Abre bajo la política de acceso del informe igual que el PDF (enlace, contraseña, vencimiento, firmado, privado, el enlace QR del PDF y los enlaces compartidos), responde text/markdown; charset=utf-8 y lleva el título como nombre (<title>.md); agrega ?download=1 para descargarlo. En un informe signed lleva la misma firma que pdf_url. El menú de acciones de la página alojada tiene Download as Markdown, y las descargas se cuentan en las estadísticas del informe como las del PDF, en su propio contador.

GET /api/v1/reports/{id}/markdown

Devuelve el texto a una clave de API: 200 con text/markdown; charset=utf-8. Un informe hecho sin versión en Markdown recibe 404 markdown_not_requested; uno que todavía se está generando, o que falló, 409 report_not_ready.

Cómo se ve
---
title: "Fleet inspection 2025"
template: fleet-inspection-full-year@1
language: en
report_id: rpt_2Qq3i6Fz6JN8pDfBkPyzcu
created_at: 2026-10-09T02:41:07Z
pages: 271
mode: test
url: https://reports.pressfox.io/report/UXO3Hy5NvORcqsAeKJIUcg
---

> TEST report — not valid as a record.

# Fleet inspection report

## MV OCEAN HORIZON · Genoa, Italy · 2025-01-20

- **Date:** 20 Jan 2025
- **Result:** Satisfactory

### Checklist

- [x] Means of escape unobstructed — Pass
- [ ] Main engine and auxiliaries — Fail

| ID | Area | Description | Severity | Status |
|---|---|---|---|---|
| 3.1 | Machinery | Emergency generator failed to take load within 45 seconds. | Observation | Rectified |

*Photo 13 (p. 116): Internal ladders / stairs — Cargo Hold No.2*
  • Front matter (YAML): title, template como key@N, language, report_id, created_at, pages, mode, y la url alojada solo cuando el tipo de acceso del informe es link. El cuerpo de un informe de prueba empieza con > TEST report — not valid as a record.
  • Cuerpo, en el orden de lectura del documento impreso y en el idioma del informe (las traducciones de la plantilla y las palabras propias de Pressfox, como en el PDF): portadas como texto; títulos por nivel; texto con negritas y cursivas; avisos como citas; grillas de campos, pares clave-valor, partes y totales como listas - **Label:** value; tablas como tablas de Markdown con sus filas de totales y texto vacío; listas de verificación como listas de tareas (- [x] … — Pass, - [ ] … — Fail, N/A como elemento simple); tarjetas de estado, puntaje, medidor y KPI en una línea cada una; listas y líneas de tiempo como listas; gráficos como sus datos en una tabla y una línea que indica el tipo de gráfico, nunca una imagen; firmas como **Signed:** name, role, date; resúmenes de IA seguidos de Written by AI; fotos como *Photo N (p. X): caption*, con un enlace a la imagen alojada solo en informes link, y el anexo de fotos como una lista de leyendas.
  • Se omite: encabezados y pies repetidos, números de página, el índice, marcas de agua, logos, códigos QR y separadores (mobiliario de página, no contenido). Un código de barras con etiqueta conserva su valor como texto.
  • Respaldo en HTML y escape. Una tabla cuyas celdas tienen saltos de línea se escribe como una <table> de HTML, con cada celda escapada. Los caracteres de Markdown de tus valores se escapan, también las barras verticales en las celdas; nada en los datos puede inyectar HTML.

El Markdown se guarda cifrado junto al HTML y al PDF y se borra con ellos: a las 24 horas en los informes de prueba, al final de la retención del plan en los demás, o cuando se borra el informe.

Verificar un documento

Desde Standard, el PDF de cada informe real recibe un registro de verificación cuando está listo: su huella SHA-256, el nombre del emisor y la hora de emisión. Cualquiera que tenga el PDF puede comprobarlo en /verify, en app.pressfox.io o en tu propia dirección de informes; el navegador calcula la huella y el archivo nunca se sube. Cuando tu espacio de trabajo tiene su propia dirección de informes, cada PDF imprime una línea que dice dónde comprobarlo, en esa dirección. Los informes de prueba y los de retención cero no reciben registro.

La página consulta GET /api/verification/check/{sha256}: pública, sin clave, con límite de solicitudes por dirección y nunca en caché. Responde { "status": "genuine" | "replaced" | "revoked" | "not_found" }, con issuer e issued_at para un archivo conocido, replaced_at cuando una corrección lo reemplazó y revoked_at con el reason del emisor cuando fue revocado. Un valor que no tiene 64 caracteres hexadecimales recibe 422 invalid_value. En tu propia dirección de informes responde solo por los documentos de tu espacio de trabajo.

No encontrado significa que Pressfox no emitió el archivo o que cambió después de emitirse: anotar un PDF o guardarlo de nuevo cambia su huella, y una copia firmada con tu proveedor de firmas es un archivo nuevo (ver firmas, en inglés). Un registro no guarda contenido del informe y se conserva 10 años, hasta que se borra el espacio de trabajo. La verificación prueba que un archivo no cambió desde que se emitió; no es una firma ni un sello electrónico.

Plantillas y contratos

GET /api/v1/templates?cursor=&limit=

Lista las plantillas del espacio de trabajo, de la actualizada más recientemente a la más antigua, como { items, next_cursor }. Cada elemento es { id, key, name, current_version, source_language, languages }, donde current_version es { id, number, kit_version } de la versión publicada con la que se generan los informes, o null para una plantilla que nunca se publicó. versions lista sus versiones publicadas de la más nueva a la más vieja (como máximo 50) como { number, current, retired, created_at }, así ves qué números puedes fijar. source_language es el idioma en que está escrita esa versión y languages los idiomas en que se puede crear un informe: el original, y luego cada idioma con traducciones (ver idiomas); null y [] antes de la primera publicación. limit va de 1 a 200, por defecto 50. Las plantillas archivadas no se listan.

GET /api/v1/templates?limit=50

{
  "items": [
    { "id": "tpl_01j9xk2m8", "key": "clinic-visit-summary", "name": "Clinic visit summary", "current_version": { "id": "ver_01j9xk5p2", "number": 4, "kit_version": "1.4.0" }, "source_language": "en", "languages": ["en", "es", "pt"],
      "versions": [
        { "number": 4, "current": true, "retired": false, "created_at": "2026-10-06T15:02:11.204118+00:00" },
        { "number": 3, "current": false, "retired": false, "created_at": "2026-09-21T10:40:52.880315+00:00" },
        { "number": 2, "current": false, "retired": true, "created_at": "2026-09-02T08:12:30.117902+00:00" },
        { "number": 1, "current": false, "retired": true, "created_at": "2026-08-28T13:55:04.661020+00:00" }
      ] },
    { "id": "tpl_01j9xkz0a", "key": "invoice", "name": "Invoice (draft)", "current_version": null, "source_language": null, "languages": [], "versions": [] }
  ],
  "next_cursor": null
}

GET /api/v1/brands

Lista las marcas del espacio de trabajo, de la más antigua a la más nueva, como { items }. Cada elemento es { id, key, name, is_default }. Envía una key como "brand" al crear un informe. La marca por defecto es con la que empiezan las plantillas nuevas en el editor; un informe sin "brand" conserva el tema propio de la plantilla. Las marcas se diseñan en la app (Free y Standard tienen una, Pro ilimitadas); este endpoint es de solo lectura.

GET /api/v1/brands
Authorization: Bearer pf_live_…

200 OK
{ "items": [ { "id": "brd_01j9xk3n1", "key": "tolland", "name": "Tolland & Reese", "is_default": true } ] }

GET /api/v1/templates/{key or id}/contract

Devuelve el contrato de la versión publicada actual, o de la versión N cuando {key or id} termina en @N o la consulta dice ?version=N (ambos con números distintos es 422 invalid_value en version). El encabezado X-Pressfox-Template-Version indica la versión que recibiste. schema es el JSON Schema contra el que la API valida los datos, inferido del ejemplo de la versión con cada ruta que lee un widget marcada como obligatoria; reads lista esas rutas, con puntos y con los índices de arrays colapsados; unread lista las hojas del ejemplo que ningún widget lee. Genera tipos a partir de schema, o valida de tu lado antes de enviar. Una plantilla sin versión publicada recibe 422 template_not_published; una versión desconocida 404 version_not_found, una retirada 410 version_retired.

GET /api/v1/templates/clinic-visit-summary/contract          (the current version)
GET /api/v1/templates/clinic-visit-summary@3/contract        (version 3; ?version=3 is the same)

HTTP/1.1 200 OK
X-Pressfox-Template-Version: 4

{
  "schema": {
    "type": "object",
    "additionalProperties": true,
    "required": ["patient", "results"],
    "properties": {
      "patient": { "type": "object", "additionalProperties": true, "required": ["name"], "properties": { "name": { "type": "string" }, "dob": { "type": "string", "format": "date" } } },
      "results": { "type": "array", "items": { "type": "object", "additionalProperties": true, "required": ["test", "value"], "properties": { "test": { "type": "string" }, "value": { "type": "number" } } } }
    }
  },
  "reads": ["patient.name", "results.test", "results.value"],
  "unread": ["patient.dob"]
}

Versiones y fijación

Cada publicación congela la plantilla en una versión nueva y numerada: 1, 2, 3 y así. Una versión nunca cambia después de publicada: su diseño, su contrato, su patrón de título, sus opciones y sus traducciones quedan como estaban, y cada informe registra la versión con que se hizo (template_version). Publicar convierte la nueva versión en la actual.

  • Por defecto: la versión actual. "template": "clinic-visit-summary" genera la versión actual, como siempre. Cuando una publicación cambia el contrato, quienes envían la forma anterior empiezan a recibir 422.
  • Fijar: key@N. "template": "clinic-visit-summary@3" (o "template_version": 3) genera la versión 3 sea cual sea la actual. Las claves no pueden contener @, así la referencia no es ambigua; también funciona con ids (tpl_…@3).
  • Las versiones conviven. Un sistema puede enviar @3 mientras otro envía @4 o nada, al mismo tiempo. Cada informe se valida contra su propia versión y se genera con ella: contrato, patrón de título, idiomas, opciones, imágenes y resúmenes de IA.
  • Un contrato por versión. GET /api/v1/templates/clinic-visit-summary@3/contract da el esquema contra el que valida la versión 3; GET /api/v1/templates lista los números que tiene cada plantilla.
  • Retirar. En la app, una versión que ya no debe usarse se puede retirar. Los informes nuevos fijados a ella reciben 410 version_retired, cuyo mensaje indica la versión actual a la que pasar; los informes ya hechos con ella conservan sus enlaces y PDF. La versión actual no se puede retirar, y una versión retirada se puede volver a activar.
  • Hacer actual: revertir. En la app, cualquier versión publicada que no esté retirada puede volver a ser la actual. Las solicitudes sin versión la generan desde ese momento; no se vuelve a publicar nada ni se crea un número nuevo.

Recomendación. Fija la versión en producción (key@N), y omítela mientras construyes con una clave de prueba. Cuando se publique una versión nueva, obtén su contrato, actualiza tus datos y mueve la fijación cuando estés listo. El diálogo de publicación de la app muestra cómo cambió el contrato respecto de la versión anterior.

Una solicitud fijada a la versión 3
POST /api/v1/reports
Authorization: Bearer pf_live_7f3a…
Content-Type: application/json

{
  "template": "clinic-visit-summary@3",
  "payload": { "patient": { "name": "A. Rivera" }, "results": [ { "test": "HbA1c", "value": 6.1 } ] }
}
Una versión desconocida, y una retirada
HTTP/1.1 404 Not Found

{
  "error": {
    "type": "not_found",
    "code": "version_not_found",
    "message": "Template clinic-visit-summary has no version 7. It has 4, 3, 2, 1. Send one of those, or leave the version out to use the current one (4).",
    "field": "$.template",
    …
  }
}

HTTP/1.1 410 Gone

{
  "error": {
    "type": "gone",
    "code": "version_retired",
    "message": "Version 1 of clinic-visit-summary is retired. Move to clinic-visit-summary@4, the current version, or ask an admin to unretire it.",
    "field": "$.template",
    …
  }
}

Imágenes

Envía cada imagen como un enlace al lugar donde ya está: { "src": "https://your-bucket.s3.eu-west-1.amazonaws.com/visits/48213/hull.jpg?X-Amz-Signature=…", "caption": "Forward hull" }. Los buckets privados funcionan con URL prefirmadas; unos minutos de validez alcanzan. Pressfox descarga cada imagen una vez, cuando empieza el render del informe, y guarda una copia con el informe, así el informe nunca vuelve a depender del enlace: sigue funcionando después de que el enlace vence, y se borra con el informe.

  • Dónde. Solo se descargan los campos que la plantilla usa como imágenes (en el contrato, "description": "asset id"). Una URL en cualquier otra parte de los datos queda como texto.
  • Qué. Solo https. JPEG, PNG o WebP, según los bytes del archivo y no su Content-Type. Hasta 20 MiB por imagen, 500 imágenes distintas y 1 GiB en total por informe. Se siguen hasta tres redirecciones; cada dirección se revisa, y nunca se contactan direcciones privadas, de loopback ni de metadatos de la nube.
  • Cuándo. La forma de las URL se revisa al crear el informe (422 que indica el campo). La descarga ocurre en el render: hasta 16 a la vez, la misma URL descargada una sola vez, y un almacenamiento ocupado o con fallas (429, 5xx, timeouts) se reintenta dos veces. Un informe con más de 8 imágenes va a la cola grande, así que espera un 202 y consulta o usa el webhook.
  • Fallas. Una imagen que no se puede descargar hace fallar el informe con un error que indica su ruta y el motivo, p. ej. $.inspections[3].photos[1].src could not be fetched: the storage answered 403: the link may have expired or need signing again. El enlace en sí nunca se escribe en un error ni en un registro.

La misma foto enviada en muchos informes se guarda una sola vez. Las imágenes que reutilizas en todos lados (logos, firmas) se pueden subir una vez como archivos y referenciar como asset:<id>; ambas formas se pueden mezclar en los mismos datos.

Archivos (assets)

POST /api/v1/assets

Sube una imagen una vez para usarla en muchos envíos de datos: logos, firmas, membretes. Para fotos que ya están en tu almacenamiento, envía sus URL en su lugar (imágenes). Dos formas de cuerpo: JSON { content_type, data, name? } con data en base64 (se acepta y se quita un prefijo data:…;base64,), o multipart/form-data con una parte file y un campo name opcional. Tipos aceptados: image/png, image/jpeg, image/webp, image/gif, image/svg+xml; los bytes tienen que coincidir con el tipo declarado, y un SVG no puede llevar scripts ni manejadores de eventos. Máximo 10 MiB por archivo. Los archivos se deduplican por hash del contenido dentro del espacio de trabajo: los bytes nuevos responden 201, los que ya están guardados responden 200 con el archivo existente. Los bytes guardados cuentan para el cupo de archivos de tu plan; si lo superas, 402 asset_bytes_limit.

Subida, JSON
POST /api/v1/assets
Authorization: Bearer pf_live_7f3a…
Content-Type: application/json

{ "content_type": "image/jpeg", "data": "<base64>", "name": "hull-forward.jpg" }

HTTP/1.1 201 Created

{
  "asset_id": "asset:ast_01j9xkd8m3",
  "bytes": 184322,
  "content_type": "image/jpeg",
  "name": "hull-forward.jpg",
  "width": 1600,
  "height": 1200,
  "created_at": "2026-10-07T09:10:12.330117+00:00"
}
Subida, multipart
curl https://api.pressfox.io/api/v1/assets \
  -H "Authorization: Bearer pf_live_7f3a…" \
  -F "file=@hull-forward.jpg;type=image/jpeg" \
  -F "name=hull-forward.jpg"

Referencia el archivo como asset:<id> en cualquier lugar de los datos donde vaya un string (o en un tema de marca). Cada referencia se resuelve antes del render; un id que no está en tu espacio de trabajo queda sin resolver y el bloque de imagen muestra su espacio vacío; el informe igual se genera, así que revisa los ids antes de enviar. GET /api/v1/assets/{id} devuelve el mismo objeto que la subida, con o sin el prefijo asset: en el id.

Uso en los datos
{ "template": "vessel-inspection", "payload": { "photos": [ { "image": "asset:ast_01j9xkd8m3", "caption": "Forward hull, port side" } ] } }

Errores

Cada error, de cada endpoint, usa un mismo formato y una misma voz. type es la clase general, code es estable y está documentado, message es para personas (en inglés), field es una ruta JSON dentro de tu solicitud cuando el error es sobre un campo, expected y received aparecen cuando hay algo que comparar, y docs_url enlaza a la entrada del catálogo. Las claves sin nada que decir se omiten en lugar de enviarse como null. Cita el request_id cuando escribas a soporte.

HTTP/1.1 422 Unprocessable Content

{
  "error": {
    "type": "validation_error",
    "code": "wrong_type",
    "message": "$.results[0].value should be a number. You sent a string. I'll be here.",
    "field": "$.results[0].value",
    "expected": "number",
    "received": "string",
    "docs_url": "https://pressfox.io/docs/errors#wrong_type",
    "request_id": "req_01j9xk8d2"
  }
}

Los límites del plan son una clase propia: type: "limit_exceeded", HTTP 402, y un código que indica el límite. field es el nombre del límite o el campo de la solicitud que lo activó. upgrade_plan indica el plan más bajo que levanta el rechazo ("starter", "standard", "pro" o "enterprise"), y el mensaje nombra el mismo plan; no aparece cuando ningún plan lo levantaría.

HTTP/1.1 402 Payment Required

{
  "error": {
    "type": "limit_exceeded",
    "code": "signed_urls_not_included",
    "message": "Signed and private links are not included in the Standard plan. The Pro plan lifts it.",
    "field": "$.access.kind",
    "upgrade_plan": "pro",
    "docs_url": "https://pressfox.io/docs/errors#signed_urls_not_included",
    "request_id": "req_01j9xk9f1"
  }
}
CódigoEstadoSignificado
missing_field422Falta un campo obligatorio.
wrong_type422Un campo tiene el tipo JSON equivocado.
empty_array422Una lista que debe tener elementos está vacía.
invalid_value422Un valor no cumple una regla de formato, enumeración o rango, o un valor de encabezado o de consulta no es correcto.
unknown_option422options indica un interruptor que la versión no declara.
template_not_published422La plantilla no tiene versión publicada.
unsupported_asset_type, asset_too_large422El archivo subido no es un tipo de imagen aceptado, o pesa más de 10 MiB.
invalid_request400El cuerpo no se pudo leer como JSON, o la solicitud está mal formada de una forma que ningún otro código cubre.
unauthorized401Falta la clave de API o no es válida.
not_found404No existe esa plantilla, marca, informe o archivo en este espacio de trabajo y modo.
version_not_found404La plantilla no tiene una versión con el número que fijaste.
version_retired410La versión que fijaste está retirada; pasa a la que indica el mensaje.
conflict409Clave de idempotencia reutilizada con otra solicitud, o todavía en curso.
payload_too_large413Los datos pesan más de 8 MiB.
rate_limited429Demasiadas solicitudes este minuto desde la clave o el espacio de trabajo; espera Retry-After segundos. Ver límites de solicitudes.
live_units_limit, report_storage_limit, *_limit, *_not_included402Un límite o una función del plan. El código indica cuál, y upgrade_plan el plan que lo levanta.

La redacción completa y qué hacer en cada caso están en el catálogo de errores (en inglés). Además de rate_limited, la API responde 429 al compartir por encima del tope diario (share_limit), y una página de informe alojado ante demasiadas contraseñas incorrectas. El Playground de la app tiene sus propios límites (60 solicitudes por miembro por hora, 10 ejecuciones con IA por espacio de trabajo al día: playground_ai_daily_limit).

Límites de solicitudes

Las solicitudes a /api/v1 se cuentan en ventanas de un minuto que empiezan en el minuto exacto, por clave de API y entre todas las claves de un espacio de trabajo. Las claves de prueba y reales comparten las ventanas del espacio de trabajo; una app de IA conectada cuenta como la clave de su conexión. Las solicitudes que fallan la autenticación no se cuentan, y las páginas de informes alojados, los PDF y los envíos de webhooks no son solicitudes a la API.

VentanaPor clave de APIPor espacio de trabajo (todas las claves)
Todas las solicitudes600 por minuto1,200 por minuto
Creación de informes (POST /api/v1/reports)120 por minuto240 por minuto

Una creación de informe cuenta en ambas filas. Cada respuesta contada lleva tres encabezados para la ventana más cercana a su límite: RateLimit-Limit (el tamaño de la ventana), RateLimit-Remaining (solicitudes que quedan en ella) y RateLimit-Reset (segundos hasta que se reinicia). Una solicitud por encima de un límite no se cuenta y se rechaza con 429 rate_limited y un encabezado Retry-After en segundos; espera ese tiempo y envíala de nuevo, con la misma Idempotency-Key si creaba un informe. Los lotes grandes van más rápido repartidos a lo largo del minuto que enviados de golpe. Si tu volumen necesita más, escríbenos.

Webhooks

Los espacios de trabajo de pago registran endpoints HTTPS en la app (Settings → Webhooks): 1 en Starter, hasta 10 en Standard, los que necesite en Pro y Enterprise; en Free la app responde 402 webhooks_not_included. Cada endpoint se suscribe a cualquiera de los dos eventos, report.ready y report.failed. Los envíos son livianos: ids, estado y URL, nunca datos del informe. Los eventos de informes hechos con una clave de prueba llevan "mode": "test", así que filtra por eso si tu endpoint solo debe actuar sobre informes reales; los informes enviados desde el Playground de la app nunca se entregan. Los informes que un miembro crea desde un formulario en la app son informes reales y se entregan, con "source": "dashboard" ("api" para los que pidió tu integración), así un endpoint puede ignorar los informes que no pidió.

CampoNotas
type"report.ready" o "report.failed"
idEl id del evento, evt_…, el mismo valor que el encabezado webhook-id. Deduplica con él; un reenvío lleva el mismo id.
created_atCuándo se registró el evento, RFC 3339.
data{ report_id, status, url, pdf_url, markdown_url, template_id, template_key, template_version, mode, source } (markdown_url es null cuando el informe no tiene versión en Markdown). Consulta el informe para obtener las páginas, las unidades y el texto del error.

Los envíos se firman en el formato Standard Webhooks. Tres encabezados: webhook-id, webhook-timestamp (segundos Unix) y webhook-signature, cuyo valor es v1,<base64 HMAC-SHA256>. La clave es el secreto del endpoint que se muestra una vez al crearlo, whsec_ seguido de base64; quita el prefijo y decodifícalo de base64. El texto firmado es id + . + timestamp + . + los bytes del cuerpo tal como llegan, así que verifica contra los bytes que recibiste, no contra un objeto vuelto a serializar. Hoy se envía una sola firma; no hay rotación de secretos. Rechaza marcas de tiempo que difieran más de cinco minutos de tu reloj.

Envío
POST https://example.com/hooks/pressfox
content-type: application/json
user-agent: Pressfox-Webhooks/1.0
webhook-id: evt_01j9xkf0s7
webhook-timestamp: 1791364443
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{"type":"report.ready","id":"evt_01j9xkf0s7","created_at":"2026-10-07T09:14:03.402911+00:00","data":{"report_id":"rpt_01j9xk7q4","status":"ready","url":"https://app.pressfox.io/report/k7Qm2Zb9pX4nT1vW8sLq0A","pdf_url":"https://app.pressfox.io/report/k7Qm2Zb9pX4nT1vW8sLq0A/pdf","markdown_url":null,"template_id":"tpl_01j9xk2m8","template_key":"clinic-visit-summary","template_version":4,"mode":"live","source":"api"}}
Verificar (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

// secret: the "whsec_…" value shown once when the endpoint was created.
// headers: lower-cased request headers. rawBody: the body as received, a Buffer, not re-serialised.
export function verify(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  if (!/^\d+$/.test(ts ?? "")) return false;
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(ts)) > 300) return false;                   // the sender's own tolerance is 5 minutes

  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const signed = Buffer.concat([Buffer.from(`${id}.${ts}.`), rawBody]); // "id.timestamp." + body bytes
  const expected = createHmac("sha256", key).update(signed).digest();

  // One "v1,<base64>" value is sent today; the header is still parsed as a space-separated list.
  return (headers["webhook-signature"] ?? "").split(" ").some((part) => {
    const [version, sig] = part.split(",");
    if (version !== "v1" || !sig) return false;
    const given = Buffer.from(sig, "base64");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

Responde con cualquier 2xx en menos de 10 segundos, medidos desde la conexión hasta el último byte leído; las redirecciones no se siguen y cuentan como falla, y el cuerpo de la respuesta se ignora después de 4 KB. Los envíos fallidos se reintentan a 1 minuto, 5 minutos, 30 minutos, 2 horas, 6 horas, 12 horas y 24 horas del intento anterior. Un endpoint que lleva 3 días fallando sin un solo éxito se desactiva: los reintentos pendientes se detienen y no se le envían eventos nuevos. La app lo muestra como desactivado con cuándo y por qué (y lo anota en el registro de auditoría). Corrige el receptor y elige Enable again en el endpoint (propietarios y administradores): conserva su URL y su secreto de firma, su conteo de fallas vuelve a cero y los 3 días empiezan de nuevo desde ese momento. Los eventos perdidos mientras estuvo desactivado no se envían solos; la app muestra cada envío con sus intentos y te deja reenviar uno.

Modo de prueba y modo real

El modo lo define la clave. Las claves de prueba generan documentos con la marca de agua TEST y no cuentan nada contra tu plan. Las claves reales generan documentos limpios en los planes de pago (Free lleva una marca de agua PRESSFOX) y miden unidades: cada informe real se cobra ceil(pages / 50) unidades una vez, al generarse. Las plantillas, marcas y archivos se comparten entre modos; los informes no: una clave de prueba lista, lee y borra solo informes de prueba, y una clave real solo los reales. El registro de idempotencia es por espacio de trabajo, así que la misma clave enviada desde una clave de prueba y desde una real es un conflicto, no una repetición.

Los informes de prueba se guardan 24 horas en todos los planes, vengan de una clave de prueba o del Playground de la app: un barrido cada hora los borra, y sus enlaces, códigos QR y envíos se detienen al mismo tiempo (la página alojada dice Available until). Los informes reales se guardan como dice la sección siguiente. Los informes de prueba de una clave de prueba se envían a tus webhooks con "mode": "test"; los enviados desde el Playground nunca.

Cuánto tiempo se guardan los informes

En los planes de pago, un informe real no tiene fin mientras la suscripción del espacio de trabajo esté activa: su retain_until es null. Cuando la suscripción termina (se cancela, o el espacio pasa a Free), cada informe sin fin recibe uno, 30 días después; suscribirse de nuevo dentro de esos días lo quita. Cambiar entre planes de pago no cambia nada. En Free, los informes reales se guardan durante 7 días; en Enterprise, según acuerdo. Cuando vence el plazo de un informe, su página, su PDF, su Markdown, sus enlaces, su código QR y sus envíos se detienen juntos, y el informe se borra.

Un plazo más corto es tu decisión. Un propietario fija la retención de informes del espacio de trabajo en la app (Settings → Reports, report_retention_days: de 1 a 3,650 días, o vacío para "mientras la suscripción esté activa"); se aplica a los informes creados después del cambio. retain_days en POST /api/v1/reports fija el de un informe, como máximo el del espacio de trabajo. El fin de un informe se puede adelantar en la app, nunca atrasar.

El almacenamiento de informes tiene un límite por plan: 1 GB en Free, 10 GB en Starter, 50 GB en Standard, 500 GB en Pro y 2 TB en Enterprise. Un informe real por encima de él recibe 402 report_storage_limit; nunca se borra nada para hacer lugar, así que borra los informes que ya no necesites, agrega almacenamiento en un plan de pago o cambia de plan. Los informes de prueba nunca cuentan.

Usa Pressfox desde agentes de IA (MCP)

El servidor MCP de Pressfox permite que un asistente de IA como Claude, ChatGPT o cualquier otro cliente de Model Context Protocol haga lo mismo que tu integración: encontrar la plantilla, leer su contrato, crear el informe y devolver el enlace. Hay dos formas de conectarse, con las mismas herramientas: por URL, donde inicias sesión en Pressfox y apruebas al asistente (sin clave que pegar), o localmente, un pequeño paquete de Python que se ejecuta en tu computadora con una clave del espacio de trabajo. En ambos casos el asistente no puede hacer nada que tu aprobación o tu clave no permitan, y ningún modelo se ejecuta de nuestro lado: el asistente es tuyo.

Conectar por URL (OAuth)

La URL del servidor es https://mcp.pressfox.io/mcp. En Claude (claude.ai, escritorio o móvil): Settings, Connectors, Add custom connector, pega la URL y luego Connect. En Claude Code: claude mcp add --transport http pressfox https://mcp.pressfox.io/mcp, y luego /mcp para iniciar sesión. Otros asistentes que admiten servidores MCP remotos con OAuth usan la misma URL.

Se abre una página de Pressfox. Indica la app, la dirección a la que te devuelve y lo que la app puede hacer. Elige el espacio de trabajo y el modo, y aprueba:

  • Prueba genera informes con marca de agua que no se miden, la forma correcta de empezar. Los editores y roles superiores pueden conectarse en modo de prueba.
  • Real genera informes reales que cuentan para tu plan. Solo administradores y propietarios pueden conectarse en modo real.

La app recibe tres permisos: leer plantillas y sus contratos (templates:read), leer informes (reports:read), y crear, enlazar y compartir informes (reports:write). No puede borrar informes, cambiar plantillas, ver otros espacios de trabajo ni administrar miembros, claves o facturación. Nunca ve una clave de API: tiene tokens de corta duración vinculados al servidor MCP de Pressfox, que el servidor revisa en cada llamada. Para desconectarla, abre Settings, Security, Connected AI apps en Pressfox; la app deja de funcionar al instante. Si dejas el espacio de trabajo o pierdes el rol que requiere el modo, la conexión también se detiene.

Para desarrolladores de clientes: el servidor sigue la especificación de autorización de MCP (revisión 2026-07-28). Los metadatos del recurso protegido están en https://mcp.pressfox.io/.well-known/oauth-protected-resource/mcp; el servidor de autorización es https://api.pressfox.io (metadatos en /.well-known/oauth-authorization-server). Admite Client ID Metadata Documents y registro dinámico de clientes, el flujo de código de autorización con PKCE (solo S256), indicadores de recurso, iss en la respuesta de autorización, tokens de actualización rotativos y revocación de tokens.

Ejecutarlo localmente con una clave

Configura el servidor local solo con variables de entorno: PRESSFOX_API_KEY (obligatoria; una clave de prueba genera informes con marca de agua que no se miden, la forma correcta de empezar), PRESSFOX_API_URL (https://api.pressfox.io) y PRESSFOX_TIMEOUT_S (por defecto 60). La clave nunca se imprime ni se registra, y los datos tampoco. Requiere uv; pressfox-mcp --check verifica la URL y la clave e imprime el espacio de trabajo.

Claude Desktop, claude_desktop_config.json
{
  "mcpServers": {
    "pressfox": {
      "command": "uvx",
      "args": ["--from", "/path/to/pressfox-mcp", "pressfox-mcp"],
      "env": {
        "PRESSFOX_API_KEY": "pf_test_…",
        "PRESSFOX_API_URL": "https://api.pressfox.io"
      }
    }
  }
}
Claude Code
claude mcp add pressfox \
  --env PRESSFOX_API_KEY=pf_test_… \
  --env PRESSFOX_API_URL=https://api.pressfox.io \
  -- uvx --from /path/to/pressfox-mcp pressfox-mcp
HerramientaLlama aDevuelve
list_templates()GET /api/v1/templatesLa clave, el id y el nombre de cada plantilla, si está publicada, y sus números de versión (la actual y las retiradas marcadas).
list_brands()GET /api/v1/brandsLa clave y el nombre de cada marca y si es la marca por defecto; pasa una clave a create_report como brand.
get_template_contract(template, version?)GET /api/v1/templates/{key}/contractLa versión que describe, los campos obligatorios, la ruta y el tipo de cada campo (los campos de imagen marcados) y un ejemplo de datos con la forma correcta.
create_report(template, payload, template_version?, title?, language?, brand?, access?, markdown=true, wait_seconds=10, idempotency_key?)POST /api/v1/reports (pidiendo Markdown salvo markdown=false), y luego GET mientras se generareport_id, status, url, pdf_url, markdown_url, pages, title, y error cuando el render falló.
get_report(report_id, include_markdown=false)GET /api/v1/reports/{id}, y …/markdown con include_markdownLa misma vista del informe; con include_markdown, el Markdown del informe en línea (hasta 200,000 caracteres, el enlace si es más largo), para que el asistente pueda responder preguntas sobre él.
list_reports(template?, status?, limit=20)GET /api/v1/reportsLos informes, del más nuevo al más viejo.
share_report(report_id, emails, duration="7d", message?)POST /api/v1/reports/{id}/sharesEl envío y el estado de entrega de cada destinatario. Envía correos reales, así que se le indica al agente que confirme las direcciones primero.

template acepta una clave, un id o key@N para fijar una versión, como en versiones y fijación. Las imágenes van dentro de los datos como URL https, exactamente como en imágenes; no hay herramienta de subida. Cuando la API rechaza unos datos, el agente lee el mismo formato de error que leerías tú, con la ruta del campo, corrige ese campo y envía de nuevo. Los agentes que escriben código de integración en lugar de llamar herramientas pueden leer /llms.txt, que enlaza esta guía en Markdown (en inglés) y todo en un solo archivo.