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

La 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 campos
  • documents.write — crear documentos y corregir campos
  • workflows.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áginas
  • name — nombre opcional del documento; si lo omites se usa el nombre del archivo
  • template_id — id de plantilla opcional, para que los campos vuelvan con tus propios nombres
  • async — 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álido
  • 401 — sin clave, o la clave está revocada o caducada
  • 403 — la clave no tiene el permiso que necesita este endpoint
  • 404 — no hay ningún documento con ese id en este espacio
  • 429 — límite por minuto alcanzado: espera y reintenta
  • 500 — algo falló por nuestra parte; la respuesta lo indica