Desarrolladores
API de Formalini
Lee documentos y sus campos extraídos, envía documentos desde tus sistemas, corrige valores y recibe webhooks firmados cuando algo cambia. Cada ejemplo se muestra en curl, JavaScript, Python, PHP y C#.
URL base
Todos los endpoints están bajo esta URL base. Se envía y se recibe JSON.
https://api.formalini.com/api/public/v1La dirección corta api.formalini.com ya está activa; es la que aparece arriba.
Autenticación
Crea una clave API en la app, en Integraciones, y envíala como bearer token en cada petición. Cada clave pertenece a un solo espacio y solo ve los datos de ese espacio.
documents.read— leer documentos y sus camposdocuments.write— crear documentos y corregir camposworkflows.run— enviar eventos a tus webhooks cuando quieras
Cada clave tiene un límite de peticiones por minuto (60 por defecto). Las respuestas incluyen X-RateLimit-Limit y X-RateLimit-Remaining; al superarlo recibes 429 con Retry-After.
Listar documentos
GET /documents?limit=25&status=approved — documents.read
Los más recientes primero. limit acepta 1–100 (25 por defecto) y status filtra por queued, processing, review, approved, exported o failed.
curl -X GET "https://api.formalini.com/api/public/v1/documents?limit=25&status=approved" \
-H "Authorization: Bearer pf_live_your_key"{
"documents": [
{
"id": "b0f1c8d2-5a71-4c0e-9f2a-1d4b6e8c3a90",
"name": "Job sheet 4821.jpg",
"status": "approved",
"average_confidence": 0.94,
"source": "upload",
"created_at": "2026-09-11T09:12:04.000Z",
"fields": [
{ "name": "engineer", "value": "M. Byrne", "type": "text",
"confidence": 0.97, "page": 1, "valid": true }
]
}
]
}Los valores corregidos sustituyen la lectura original, así que value es siempre el valor a usar.
Obtener un documento
GET /documents/{id} — documents.read
Devuelve el documento con su categoría, proveedor, importe, fecha y todos sus campos.
curl -X GET "https://api.formalini.com/api/public/v1/documents/b0f1c8d2-5a71-4c0e-9f2a-1d4b6e8c3a90" \
-H "Authorization: Bearer pf_live_your_key"Sube un archivo y obtén sus datos
POST /documents/upload — documents.write
Envía la imagen de una página y recibe los datos estructurados en la misma respuesta. El documento aparece en la bandeja de trabajo como cualquier otro escaneo, consume un crédito de IA por página y dispara tus webhooks al terminar.
file— la imagen de la página; repite el campo una vez por página (hasta 25) para documentos de varias páginasname— nombre opcional del documento; si lo omites se usa el nombre del archivotemplate_id— id de plantilla opcional, para que los campos vuelvan con tus propios nombresasync— ponlo en true para recibir un 202 de inmediato y consultar GET /documents/{id}
curl -X POST "https://api.formalini.com/api/public/v1/documents/upload" \
-H "Authorization: Bearer pf_live_your_key" \
-F "file=@invoice-page-1.jpg" \
-F "file=@invoice-page-2.jpg" \
-F "name=Invoice 2026-0041"{
"document": {
"id": "8c2a1f76-4d33-4a1e-8f10-72b9e5c4a118",
"name": "Invoice 2026-0041",
"status": "review",
"average_confidence": 0.93,
"vendor": "Northbridge Supplies",
"amount": 1249.5,
"currency": "EUR",
"document_date": "2026-09-14",
"pages": 2,
"credits_spent": 2,
"fields": [
{ "name": "invoice_number", "value": "2026-0041", "type": "string",
"confidence": 0.98, "page": 1, "valid": true }
]
}
}Se aceptan JPEG, PNG, WebP y HEIC, hasta 100 MB por página. Leer una página tarda unos segundos: usa un tiempo de espera amplio o async=true. Un 402 significa que al espacio le faltan créditos, almacenamiento o su cupo diario, y la respuesta indica cuál.
Todavía no se aceptan PDF aquí: convierte cada página a JPEG o PNG y envíalas repitiendo el campo file. Subir un PDF en la aplicación sigue funcionando, porque las páginas se convierten en tu navegador.
Crear un documento
POST /documents — documents.write
Envía un documento del que ya tienes datos. name es obligatorio; fields es opcional y admite hasta 400 entradas. Con campos entra en revisión; sin campos, en la cola.
curl -X POST "https://api.formalini.com/api/public/v1/documents" \
-H "Authorization: Bearer pf_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Invoice 2026-0041.pdf",
"category": "invoice",
"vendor": "Northbridge Supplies",
"amount": 1249.5,
"document_date": "2026-09-14",
"fields": [
{
"name": "invoice_number",
"value": "2026-0041",
"type": "text",
"confidence": 0.99,
"page": 1
},
{
"name": "total",
"value": "1249.50",
"type": "number",
"confidence": 0.96,
"page": 1
}
]
}'{
"document": {
"id": "8c2a…",
"name": "Invoice 2026-0041.pdf",
"status": "review",
"created_at": "2026-09-18T10:02:11.000Z",
"fields": 2
}
}Actualizar un documento
PATCH /documents/{id} — documents.write
Cambia el estado o corrige valores por nombre. Poner el estado en approved, exported o review también dispara el webhook correspondiente.
curl -X PATCH "https://api.formalini.com/api/public/v1/documents/b0f1c8d2-5a71-4c0e-9f2a-1d4b6e8c3a90" \
-H "Authorization: Bearer pf_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"status": "approved",
"fields": [
{
"name": "total",
"value": "1249.50"
}
]
}'{ "ok": true, "updated_fields": 1 }Disparar tus webhooks
POST /workflows/dispatch — workflows.run
Envía un evento a todos los webhooks activos suscritos a él, para arrancar otro sistema cuando lo necesites. Eventos permitidos: document.processed, document.needs_review, document.approved, document.exported.
curl -X POST "https://api.formalini.com/api/public/v1/workflows/dispatch" \
-H "Authorization: Bearer pf_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"event": "document.approved",
"document_id": "b0f1c8d2-5a71-4c0e-9f2a-1d4b6e8c3a90",
"payload": {
"target": "erp"
}
}'{ "ok": true, "event": "document.approved", "document_id": "b0f1…" }Webhooks
Añade una URL de webhook en la app y elige los eventos. Cada envío es un POST con cuerpo JSON y una cabecera de firma, firmada con tu secreto.
POST https://your-app.example.com/hooks/formalini
Content-Type: application/json
X-Paperflow-Signature: sha256=<hex hmac of the raw body>
{ "event": "document.processed", "document_id": "b0f1…", "payload": { … } }Los envíos fallidos se reintentan hasta cinco veces con retardo creciente, y puedes reenviar cualquiera desde la app.
Verifica la firma contra el cuerpo original antes de confiar en un envío:
// Node.js
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const got = (header || "").replace(/^sha256=/, "");
return got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
# Python
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, (header or "").removeprefix("sha256="))
// PHP
$expected = hash_hmac('sha256', $rawBody, $secret);
$got = str_replace('sha256=', '', $_SERVER['HTTP_X_PAPERFLOW_SIGNATURE'] ?? '');
$ok = hash_equals($expected, $got);
// C#
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(hmac.ComputeHash(rawBodyBytes)).ToLowerInvariant();
var ok = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(header.Replace("sha256=", "")));Errores
400— el cuerpo no es JSON, o falta un valor obligatorio o no es válido401— sin clave, o la clave está revocada o caducada403— la clave no tiene el permiso que necesita este endpoint404— no hay ningún documento con ese id en este espacio429— límite por minuto alcanzado: espera y reintenta500— algo falló por nuestra parte; la respuesta lo indica