Papely API
Genera documentos PDF personalizados de forma programática. Sube una imagen, envía una plantilla junto con tus datos, después sondea hasta que termine y descarga el PDF final: todo de servidor a servidor, sin navegador ni ningún paso manual.
URL base
https://papely.app/api/public/v1GET /templates— lista plantillasGET /templates/:id— consulta una plantillaPOST /images— sube una imagenPOST /generations— inicia una generaciónPOST /generations/batch— genera en loteGET /generations/:id— consulta el estadoGET /generations/:id/document— descarga el documentoGET /generations/:id/emails— consulta las entregas de email
Visión general
La API de Papely es REST sobre HTTPS. Las peticiones y respuestas son JSON (la subida de imágenes es multipart). La generación es asíncrona: envías una petición, recibes de inmediato un identificador aceptado, después sondeas hasta que el documento esté listo y lo descargas. Una integración típica sigue cuatro pasos.
- Sube las imágenes que necesites y guarda las referencias
img_<id>que devuelven. - Envía una generación con el id de la plantilla y los datos de tus campos. Recibirás un
generationId. - Sondea el endpoint de estado hasta que la generación esté en
completed(ofailed). - Descarga el documento final desde el endpoint del documento (un único PDF, o un ZIP para un lote).
Todo está acotado al workspace al que está vinculada tu credencial. Una credencial de un workspace nunca puede ver ni actuar sobre los recursos de otro workspace.
Autenticación
Cada petición se autentica con una credencial de API presentada como bearer token en la cabecera Authorization:
Authorization: Bearer papely_sk_your_secret_hereEl propietario del workspace crea las credenciales en la Configuración del workspace, en la sección de credenciales de API. Al crear una se muestra el secreto completo (siempre empieza por papely_sk_) una única vez: cópialo en ese momento, porque no se vuelve a mostrar. Solo se guarda un prefijo corto no secreto para mostrarlo, así que un secreto perdido no se puede recuperar; crea una credencial nueva en su lugar.
Propiedades clave a tener en cuenta en tu diseño:
- Vinculada a un solo workspace. Una credencial actúa sobre —y se factura a— exactamente el workspace en el que se creó.
- Gestionada por el propietario. Solo el propietario del workspace puede crear, listar o revocar credenciales.
- Revocable. Revocar una credencial impide de inmediato que se autentique. Guarda los secretos de forma segura (por ejemplo, como variable de entorno o entrada de un gestor de secretos) y nunca los subas al control de versiones.
Una credencial ausente, mal formada, desconocida o revocada devuelven todas el mismo 401 UNAUTHENTICATED, así que un rechazo nunca revela qué secreto existe.
Descubrir plantillas
/templates/templates/:idDescubre qué enviar sin abrir la app. Estos dos endpoints de solo lectura, ambos acotados a tu workspace, devuelven los mismos identificadores que pasas en una petición de generación.
Lista todas las plantillas de tu workspace. La respuesta es un array simple; cada entrada es el id de la plantilla y su nombre:
curl https://papely.app/api/public/v1/templates \
-H "Authorization: Bearer $PAPELY_SECRET"200 OK
[
{ "id": "c65f3e23-ed50-47fd-822e-7e7a18b56b8a", "name": "Factura" },
{ "id": "66725d21-d483-4add-b80a-1174802ae0a9", "name": "Contrato" }
]Consulta una plantilla para obtener sus campos. Cada campo tiene una clave (la que usas en el objeto data), un tipo y una etiqueta; un campo de tabla lista además sus columnas como pares de clave y etiqueta:
curl https://papely.app/api/public/v1/templates/c65f3e23-ed50-47fd-822e-7e7a18b56b8a \
-H "Authorization: Bearer $PAPELY_SECRET"200 OK
{
"id": "c65f3e23-ed50-47fd-822e-7e7a18b56b8a",
"name": "Factura",
"fields": [
{ "key": "title", "type": "text", "label": "title" },
{ "key": "issued_on", "type": "date", "label": "issued_on" },
{ "key": "logo", "type": "image", "label": "logo" },
{ "key": "line_items", "type": "table", "label": "line_items",
"columns": [
{ "key": "description", "label": "Description" },
{ "key": "amount", "label": "Amount" }
] },
{ "key": "name", "type": "text", "label": "name", "system": true },
{ "key": "email", "type": "text", "label": "email", "system": true }
]
}La respuesta termina con dos columnas de sistema, name y email, marcadas con system: true. A diferencia de los campos declarados, son opcionales y puedes incluirlas en el objeto data de una petición de generación: la dirección de email que pongas ahí es el destinatario de la entrega, y ambas se convierten en las variables de interpolación {{name}} y {{email}} del asunto y el mensaje del email. Como son columnas de sistema opcionales, incluir cualquiera de ellas en data nunca se rechaza como unknown_field. Los campos de datos definidos por el autor nunca llevan esta marca.
Una plantilla desconocida, o que pertenece a otro workspace, devuelve un 404 TEMPLATE_NOT_FOUND que no revela nada: idéntico a una plantilla que no existe, así que quien llama nunca puede sondear ids de otro workspace.
Encontrar tus identificadores
Para generar un documento necesitas tres tipos de identificador desde la interfaz del workspace. Abre la plantilla en Papely y usa su diálogo Detalles de la API, que lista cada valor con un control para copiarlo:
- el id de plantilla — se pasa como
templateId; - cada clave de campo y su tipo — las claves de tu objeto
data; - para un campo de tipo tabla, sus columnas en orden — cada celda de una fila de la tabla se corresponde con una columna por posición.
Una clave de campo es el nombre del campo definido en la plantilla. Renombrar un campo en la plantilla cambia su clave de API. Si renombras un campo, actualiza la clave correspondiente en tus peticiones: la clave antigua se rechaza como desconocida, y la nueva cae en silencio al valor en blanco/por defecto hasta que la envíes. Renombrar la cabecera de una columna no cambia la forma de la tabla: las celdas se corresponden con las columnas por posición, así que solo reordenar o añadir/quitar columnas las desplaza.
También puedes obtener todo esto de forma programática: GET /templates lista tus plantillas y GET /templates/:id devuelve las claves de campo, los tipos y las claves de columna de tabla de una plantilla — los mismos valores que muestra el diálogo (consulta descubrir plantillas).
Proporcionar los datos de los campos
El objeto data de una petición de generación se indexa por clave de campo, una entrada por cada campo de la plantilla. Envía lo que tengas:
- Todos los campos declarados son opcionales. Un campo omitido (o un string vacío en un campo escalar) se genera en blanco, o con el valor por defecto de la plantilla si lo tiene.
- Sin claves de más. Una clave que no corresponde a un campo declarado se rechaza con la razón
unknown_fielden lugar de ignorarse en silencio. - Todos los valores escalares son strings JSON. Los valores de texto, fecha y qr, y cada celda de tabla, son strings (una celda de tabla también puede ser null, es decir, en blanco): enviar un número (por ejemplo
1200en lugar de"1200") se rechaza con la razóninvalid_value.
Las entradas name y email marcadas con system: true en el detalle de la plantilla son columnas de sistema opcionales que puedes incluir en data —el destinatario de la entrega y las variables de interpolación—, así que nunca se cuentan como unknown_field.
Los cinco tipos de campo aceptan:
| Tipo | Valor | Ejemplo |
|---|---|---|
text | Un string JSON (se permite una cadena vacía, que se renderiza en blanco). | "Invoice #1024" |
date | Un string JSON que contiene una fecha. Se recomienda el formato ISO (YYYY-MM-DD) y se lee como una fecha de calendario simple. Tú envías el valor en bruto; la plantilla decide cómo se formatea al renderizar el PDF. | "2026-05-25" |
qr | Un string JSON no vacío. Su contenido se codifica en el código QR. | "https://papely.app/verify/abc" |
image | Una referencia de imagen (img_<id>) devuelta por POST /images. El objeto data proporciona las imágenes solo por referencia, nunca como ruta de archivo ni bytes en línea. Para ingerir una imagen desde una URL, usa antes la variante por URL de POST /images. La referencia debe pertenecer al mismo workspace que tu credencial. | "img_5bba02c7-7465-47b7-a381-3696f6033a83" |
table | Un array de filas. Cada fila es a su vez un array de celdas — una celda por columna de la tabla, en el orden de columnas de GET /templates/:id. Cada celda es un string, o null para una celda en blanco (equivalente a ""). Cada fila debe tener exactamente una celda por columna; el array puede estar vacío, y el número de filas puede variar de un documento a otro. | [["Consulting", "€1,200.00"]] |
Los valores de las celdas de tabla son texto literal: se imprimen tal cual y no son expresiones de plantilla, así que un valor como {{token}} aparece tal como está.
Subir imágenes
/imagesEnvía la imagen como multipart form-data en una parte llamada file. Se aceptan PNG y JPEG (el formato se detecta a partir de los bytes del archivo, no de su nombre). El límite de tamaño es el tamaño máximo de imagen de tu plan (10 MB por defecto). Subir una imagen consume la cuota de almacenamiento de tu workspace.
curl -X POST https://papely.app/api/public/v1/images \
-H "Authorization: Bearer $PAPELY_SECRET" \
-F "[email protected]"También puedes enviar un cuerpo JSON con un campo url y Papely descarga la imagen por ti — útil desde herramientas no-code donde el multipart es engorroso. La URL debe ser una URL https:// pública; se rechazan direcciones privadas y hosts internos. Se siguen hasta 3 redirects y la descarga expira a los 10 segundos. La imagen se obtiene una sola vez, al subirla: la referencia devuelta es una copia inmutable, así que cambios posteriores en esa URL nunca afectan a los documentos que generes.
curl -X POST https://papely.app/api/public/v1/images \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/logo.png" }'Una subida correcta devuelve la referencia opaca que usarás como valor de un campo de imagen:
201 Created
{
"reference": "img_5bba02c7-7465-47b7-a381-3696f6033a83"
}La referencia solo es válida dentro del workspace que la subió. Los rechazos no dejan bytes almacenados: algo que no es una imagen da 415 UNSUPPORTED_IMAGE_FORMAT, una imagen demasiado grande da 413 IMAGE_TOO_LARGE, y una cuota agotada da 402 STORAGE_QUOTA_EXCEEDED.
En la variante por URL, una url inutilizable (malformada, no https o apuntando a un host no permitido) da 422 IMAGE_URL_INVALID y una descarga fallida (respuesta distinta de 200, timeout, demasiados redirects) da 422 IMAGE_FETCH_FAILED.
Generar un documento
/generationsEnvía un cuerpo JSON con el templateId y el objeto data. Opcionalmente incluye una cabecera Idempotency-Key (consulta límites de tasa y reintentos seguros). El cuerpo está limitado a 5 MB; un cuerpo mayor se rechaza con 413 PAYLOAD_TOO_LARGE antes de hacer ningún trabajo.
curl -X POST https://papely.app/api/public/v1/generations \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f2a1c90-4d3e-4a1b-9c77-2b6e5f0a1d34" \
-d '{
"templateId": "YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice #1024",
"issued_on": "2026-05-25",
"verify_url": "https://papely.app/verify/abc",
"logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83",
"line_items": [
["Consulting", "€1,200.00"],
["Hosting", "€49.00"]
]
}
}'Una petición se acepta —no se completa— con un 202 y el id con el que sondeas y descargas:
202 Accepted
{
"generationId": "bd54d799-39c8-4577-a151-6a3c6afd9a39"
}Las claves de campo y el id de plantilla de arriba son ejemplos: copia los tuyos desde el diálogo Detalles de la API. Los ids son opacos; devuélvelos tal cual. Los rechazos en este paso nunca cuentan contra tu cuota: una plantilla desconocida o que no te pertenece da 404 TEMPLATE_NOT_FOUND, y una violación del contrato de datos da 422 INVALID_DATA con un array details que nombra cada campo problemático.
Envío por email opcional. Añade un objeto email de nivel superior para que Papely envíe por email el documento final a un destinatario; omítelo y no se envía nada (el comportamiento por defecto). Este bloque es solo la plantilla del mensaje: subject (de 1 a 255 caracteres), message (de 1 a 5000 caracteres) y un buttonText opcional (de 1 a 100 caracteres). El destinatario no forma parte de este bloque: proviene de tu objeto data: data.email es la dirección de destino —un documento sin ella se genera pero simplemente no se envía por email— y data.name es un nombre visible opcional.
El subject y el message interpolan {{variables}} por destinatario. Las variables permitidas son name, email y cualquier campo de la plantilla de tipo text; una variable desconocida se rechaza con 422 INVALID_DATA.
La entrega es asíncrona: la API acepta (202) y encola, y después un worker envía un email por destinatario en cuanto la generación se completa. El destinatario recibe un enlace de descarga con seguimiento que no necesita ninguna credencial de API para abrirse, y el documento sigue siendo accesible a través de GET /generations/:id/document como siempre, tenga o no email activado. El remitente es fijo ([email protected]); el reply-to se resuelve desde la dirección de respuesta verificada configurada en tu workspace y nunca se acepta del cliente. Los emails están actualmente solo en inglés.
Un envío de email fallido nunca hace fallar la generación ni consume o reembolsa cuota o packs: el PDF sigue disponible. Los destinatarios no se validan al hacer la petición: un documento con data.email en blanco se genera pero la entrega lo salta, y una dirección mal formada aparece como entrega fallida en el endpoint de emails. Los problemas de la plantilla del mensaje sí son errores de validación que devuelven 422 INVALID_DATA (nunca 500) con un array details; una variable desconocida es { "field": "email", "reason": "unknown_variable:<name>" }.
curl -X POST https://papely.app/api/public/v1/generations \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-d '{
"templateId": "YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice #1024",
"issued_on": "2026-05-25",
"verify_url": "https://papely.app/verify/abc",
"logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83",
"line_items": [
["Consulting", "€1,200.00"]
],
"email": "[email protected]",
"name": "Ada Lovelace"
},
"email": {
"subject": "Your document {{title}}",
"message": "Hi {{name}}, your invoice is ready.",
"buttonText": "Download"
}
}'Generación en lote
/generations/batchGenera muchos documentos en una sola petición. Envía un cuerpo JSON con un templateId y un array documents — de 1 a 100 entradas. Cada entrada es un objeto de datos simple: sus campos declarados indexados por nombre, más las columnas de sistema opcionales email y name. Todo el lote se convierte en una sola generación.
curl -X POST https://papely.app/api/public/v1/generations/batch \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6d1f0b2a-8c3e-4f5a-9b7c-1e2d3f4a5b6c" \
-d '{
"templateId": "YOUR_TEMPLATE_ID",
"documents": [
{ "title": "Invoice #1024", "issued_on": "2026-05-25", "verify_url": "https://papely.app/verify/abc", "logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83", "line_items": [ ["Consulting", "€1,200.00"] ] },
{ "title": "Invoice #1025", "issued_on": "2026-05-26", "verify_url": "https://papely.app/verify/def", "logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83", "line_items": [ ["Hosting", "€49.00"] ] }
]
}'El límite del cuerpo del lote es mayor que el de la generación individual —25 MB— para dar cabida a hasta 100 documentos; un cuerpo mayor se rechaza con 413 PAYLOAD_TOO_LARGE antes de hacer ningún trabajo. Un lote de 0, o de más de 100 documentos, se rechaza con 422 INVALID_DATA.
La petición se acepta —no se completa— con un 202 y un único generationId — un trabajo de generación que contiene todos los documentos:
202 Accepted
{
"generationId": "5f07913d-4566-4b53-9ac8-bf5025472cbf"
}Cada documento cuenta como una generación (un lote de N cuenta como N) sobre el mismo cupo mensual, y se cobra todo o nada: si el lote completo no cabe en la cuota restante, no se cobra nada y la petición devuelve 402 GENERATION_QUOTA_EXCEEDED. Un solo documento inválido rechaza todo el lote con 422 INVALID_DATA, y su array details nombra el documento problemático por su índice (por ejemplo documents[3].amount).
Sondea y obtén el resultado a través de los mismos endpoints de estado y documento que una generación individual. GET /generations/:id/document devuelve un archivo ZIP con los PDF cuando el lote tiene dos o más documentos, y un único PDF cuando tiene exactamente uno.
Envío por email opcional. Añade un objeto email de nivel superior para enviar por email todos los documentos. En un lote contiene solo la plantilla del mensaje compartida — subject, message y un buttonText opcional — y no lleva destinatario propio. Cada destinatario vive dentro de su documento, como las columnas de sistema opcionales email y name. Un documento sin valor en email se genera pero no se envía por email, y una dirección mal formada aparece como entrega fallida en el endpoint de emails en lugar de un error. Se aplican las mismas reglas de interpolación, entrega asíncrona, remitente fijo y solo-inglés que en una generación individual.
curl -X POST https://papely.app/api/public/v1/generations/batch \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-d '{
"templateId": "YOUR_TEMPLATE_ID",
"email": {
"subject": "Your document {{title}}",
"message": "Hi {{name}}, your invoice is ready.",
"buttonText": "Download"
},
"documents": [
{ "title": "Invoice #1024", "issued_on": "2026-05-25", "verify_url": "https://papely.app/verify/abc", "logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83", "line_items": [ ["Consulting", "€1,200.00"] ], "email": "[email protected]", "name": "Ada Lovelace" },
{ "title": "Invoice #1025", "issued_on": "2026-05-26", "verify_url": "https://papely.app/verify/def", "logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83", "line_items": [ ["Hosting", "€49.00"] ], "email": "[email protected]", "name": "Grace Hopper" }
]
}'Consultar el estado
/generations/:idSondea este endpoint con el generationId hasta que la generación termine. El status es uno de pending, processing, completed o failed. Un mensaje error solo está presente cuando el estado es failed.
curl https://papely.app/api/public/v1/generations/bd54d799-39c8-4577-a151-6a3c6afd9a39 \
-H "Authorization: Bearer $PAPELY_SECRET"200 OK
{
"id": "bd54d799-39c8-4577-a151-6a3c6afd9a39",
"status": "completed"
}Una generación fallida tiene este aspecto: lee error para conocer el motivo, e inicia una generación nueva para reintentar (con una nueva clave de idempotencia):
200 OK
{
"id": "bd54d799-39c8-4577-a151-6a3c6afd9a39",
"status": "failed",
"error": "Rendering failed"
}Un id de generación desconocido o que pertenece a otro workspace devuelve 404 GENERATION_NOT_FOUND, indistinguible de uno que quizá no puedas ver.
Obtener el PDF
/generations/:id/documentUna vez que el estado es completed, este endpoint devuelve el documento final: un único PDF para una generación de un solo documento, o un archivo ZIP con los PDF para un lote. Responde con una redirección 302 a una URL de descarga recién firmada y de corta duración, así que sigue la redirección para obtener los bytes. Cada llamada genera un enlace nuevo; vuelve a solicitar el endpoint cuando necesites uno fresco.
curl -L -o invoice.pdf \
https://papely.app/api/public/v1/generations/bd54d799-39c8-4577-a151-6a3c6afd9a39/document \
-H "Authorization: Bearer $PAPELY_SECRET"El documento se puede recuperar mediante cualquier credencial vinculada al workspace al que pertenece. Una credencial tiene alcance sobre todo su workspace —no solo sobre las generaciones que ella creó—, así que también puede leer documentos generados por otras credenciales o desde la app. Cualquier otro caso —id desconocido, otro workspace, o una generación que aún no se ha completado— devuelve el mismo 404 GENERATION_NOT_FOUND, que no revela nada.
Estado de las entregas de email
/generations/:id/emailsCuando una generación optó por el envío de email, sondea este endpoint para seguir cada mensaje. Devuelve emailEnabled —si se solicitó email— y un array emails con una entrada por destinatario (el email y el nombre del destinatario que enviaste, más las marcas de tiempo de cada mensaje). Las entregas se crean a medida que el worker las envía, así que una generación aún en proceso puede devolver un array vacío.
curl https://papely.app/api/public/v1/generations/bd54d799-39c8-4577-a151-6a3c6afd9a39/emails \
-H "Authorization: Bearer $PAPELY_SECRET"200 OK
{
"id": "bd54d799-39c8-4577-a151-6a3c6afd9a39",
"emailEnabled": true,
"emails": [
{
"email": "[email protected]",
"name": "Ada",
"status": "delivered",
"sentAt": "2026-05-25T10:00:00.000Z",
"deliveredAt": "2026-05-25T10:05:00.000Z",
"downloadedAt": "2026-05-25T12:00:00.000Z",
"error": null
}
]
}Lee el status de cada destinatario —uno de pending, sent, delivered, bounced o failed— para conocer el resultado. Descargado es una señal derivada: un downloadedAt no nulo significa que el destinatario abrió el enlace de descarga con seguimiento, y nunca cambia el estado. Un envío fallido se refleja aquí, nunca en la generación, y no mueve la cuota.
Un id de generación desconocido o que pertenece a otro workspace devuelve 404 GENERATION_NOT_FOUND, indistinguible de uno que quizá no puedas ver.
Cómputo y cuota
Cada documento generado cuenta como una generación sobre el cupo mensual de generaciones de tu workspace, el mismo contador que incrementa la interfaz manual. Las generaciones por API y manuales consumen de un mismo total mensual compartido.
- Primero se consume el cupo mensual del plan. Una vez agotado, se van descontando los packs de generaciones comprados.
- Solo cuentan las generaciones aceptadas. Una petición rechazada por validación, cuota, autenticación o cualquier otro motivo nunca se cobra.
- Un reintento idempotente (misma clave y cuerpo idéntico) se resuelve como la generación original y se cobra como mucho una vez.
- No existe ningún modo de prueba ni de vista previa: cada generación aceptada cuenta y produce un documento real.
Cuando tanto el cupo como los packs se agotan, una petición de generación devuelve 402 GENERATION_QUOTA_EXCEEDED.
Límites de tasa y reintentos seguros
Los límites de tasa se aplican por workspace en una ventana de un minuto (son valores por defecto y pueden ajustarse):
POST /generations— 10 peticiones por minuto.POST /images— 20 peticiones por minuto.GET /generations/:id,GET /generations/:id/documentyGET /generations/:id/emails— 60 peticiones por minuto (comparten un mismo presupuesto).
Superar un nivel devuelve 429 RATE_LIMITED con una cabecera Retry-After en segundos: espera ese tiempo y reintenta. Sondear el estado y descargar el documento a una cadencia normal (cada 2–3 segundos aproximadamente) se mantiene holgadamente dentro del presupuesto compartido de 60 por minuto, así que un bucle de sondeo hasta completar nunca se limita; deja margen en lugar de sondear ambos endpoints lo más rápido posible.
Idempotencia. Proporciona una cabecera Idempotency-Key en POST /generations para que un reintento sea seguro tras un error de red o un timeout. Una clave queda vinculada a la generación que creó por primera vez:
- Reenviar la misma clave con un cuerpo idéntico byte a byte devuelve el
generationIdoriginal y no crea ni cobra una segunda generación: sondéala para ver su estado. - Reutilizar la misma clave con un cuerpo diferente es un conflicto:
409 IDEMPOTENCY_KEY_CONFLICT. - Para iniciar una generación nueva —incluyendo regenerar después de que una terminara en
failed— usa una clave nueva.
Una buena clave es un UUID generado por cada intento lógico de generación y reutilizado solo para los reintentos de ese mismo intento.
Respuestas de error
Todos los errores usan el mismo sobre JSON:
{
"error": {
"code": "INVALID_DATA",
"message": "Human-readable summary",
"details": [
{ "field": "tittle", "reason": "unknown_field" }
]
},
"requestId": "a1b2c3d4-..."
}code es una cadena estable y legible por máquina: intégrate contra ella, no contra el mensaje humano ni el estado HTTP por sí solo. details solo está presente para INVALID_DATA y lista cada campo problemático con una razón: unknown_field, invalid_value o unowned_image (una referencia de imagen bien formada que el workspace no posee). requestId es un id de correlación que conviene registrar y citar en las solicitudes de soporte.
| Código | HTTP | Cuándo |
|---|---|---|
UNAUTHENTICATED | 401 | La cabecera Authorization falta o está mal formada, o el secreto es desconocido o está revocado. Todos estos casos son indistinguibles por diseño. |
INSUFFICIENT_PERMISSION | 403 | La acción requiere ser el propietario del workspace. |
TEMPLATE_NOT_FOUND | 404 | El templateId es desconocido o no pertenece al workspace al que está vinculada tu credencial. |
GENERATION_NOT_FOUND | 404 | El id de generación es desconocido, pertenece a otro workspace o aún no se puede recuperar: todo indistinguible. |
INVALID_DATA | 422 | El objeto data incumple el contrato de campos. El array details nombra cada campo problemático y por qué. |
MALFORMED_JSON | 400 | El cuerpo de la petición no es JSON válido. |
GENERATION_QUOTA_EXCEEDED | 402 | El workspace no tiene cupo mensual de generaciones restante ni packs de generaciones. |
STORAGE_QUOTA_EXCEEDED | 402 | Almacenar la imagen subida superaría la cuota de almacenamiento del workspace. |
UNSUPPORTED_IMAGE_FORMAT | 415 | El archivo subido no es PNG ni JPEG (se detecta a partir de sus bytes, no de su nombre de archivo). |
IMAGE_TOO_LARGE | 413 | La imagen subida es mayor que el tamaño máximo de imagen del plan (10 MB por defecto). |
IMAGE_URL_INVALID | 422 | La url de la subida por URL es inutilizable: malformada, no es https o apunta a una dirección no permitida. Todas las causas son indistinguibles a propósito. |
IMAGE_FETCH_FAILED | 422 | La descarga de la imagen desde la url falló: una respuesta distinta de 200, un timeout o demasiados redirects. |
PAYLOAD_TOO_LARGE | 413 | El cuerpo JSON de generación supera el límite de 5 MB. |
RATE_LIMITED | 429 | Se superó un nivel de límite de tasa por workspace. Espera lo indicado en la cabecera Retry-After y reintenta. |
IDEMPOTENCY_KEY_CONFLICT | 409 | Se reutilizó una Idempotency-Key con un cuerpo de petición diferente. |
SERVICE_UNAVAILABLE | 503 | La API está temporalmente en mantenimiento. Espera lo indicado en la cabecera Retry-After y reintenta. |
INTERNAL_ERROR | 500 | Un error inesperado del servidor. Es seguro reintentar con una nueva clave de idempotencia. |
Algunas clases comparten deliberadamente un estado HTTP pero siguen siendo distinguibles por code. En particular, las dos clases 402 — GENERATION_QUOTA_EXCEEDED (sin generaciones) frente a STORAGE_QUOTA_EXCEEDED (sin almacenamiento de imágenes) — te indican qué cuota resolver. Igualmente, las dos clases 413 (IMAGE_TOO_LARGE frente a PAYLOAD_TOO_LARGE) y las dos clases 404 (TEMPLATE_NOT_FOUND frente a GENERATION_NOT_FOUND) se separan por código. Ramifica siempre según code.
Ejemplo completo
Una secuencia completa que sube una imagen, genera un documento, sondea hasta completar y descarga el PDF. Primero, asigna a $PAPELY_SECRET el secreto de tu credencial.
1 · Sube la imagen
curl -X POST https://papely.app/api/public/v1/images \
-H "Authorization: Bearer $PAPELY_SECRET" \
-F "[email protected]"
# -> 201 { "reference": "img_5bba02c7-7465-47b7-a381-3696f6033a83" }2 · Inicia la generación
curl -X POST https://papely.app/api/public/v1/generations \
-H "Authorization: Bearer $PAPELY_SECRET" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f2a1c90-4d3e-4a1b-9c77-2b6e5f0a1d34" \
-d '{
"templateId": "YOUR_TEMPLATE_ID",
"data": {
"title": "Invoice #1024",
"issued_on": "2026-05-25",
"verify_url": "https://papely.app/verify/abc",
"logo": "img_5bba02c7-7465-47b7-a381-3696f6033a83",
"line_items": [
["Consulting", "€1,200.00"]
]
}
}'
# -> 202 { "generationId": "bd54d799-39c8-4577-a151-6a3c6afd9a39" }3 · Sondea hasta completar
# Repite cada ~2-3s hasta que status sea "completed" (o "failed").
curl https://papely.app/api/public/v1/generations/bd54d799-39c8-4577-a151-6a3c6afd9a39 \
-H "Authorization: Bearer $PAPELY_SECRET"
# -> 200 { "id": "bd54d799-39c8-4577-a151-6a3c6afd9a39", "status": "completed" }4 · Descarga el PDF
curl -L -o invoice.pdf \
https://papely.app/api/public/v1/generations/bd54d799-39c8-4577-a151-6a3c6afd9a39/document \
-H "Authorization: Bearer $PAPELY_SECRET"