Développeurs

API Formalini

Lisez les documents et leurs champs extraits, envoyez des documents depuis vos systèmes, corrigez des valeurs et recevez des webhooks signés à chaque changement. Chaque exemple est donné en curl, JavaScript, Python, PHP et C#.

URL de base

Tous les endpoints se trouvent sous cette URL de base. On envoie et reçoit du JSON.

https://api.formalini.com/api/public/v1

L'adresse courte api.formalini.com est active ; c'est celle indiquée ci-dessus.

Authentification

Créez une clé API dans l'application, rubrique Intégrations, puis envoyez-la en bearer token à chaque requête. Une clé appartient à un seul espace et ne voit que ses données.

  • documents.read — lire les documents et leurs champs
  • documents.write — créer des documents et corriger des champs
  • workflows.run — envoyer des événements à vos webhooks à la demande

Chaque clé a une limite par minute (60 par défaut). Les réponses portent X-RateLimit-Limit et X-RateLimit-Remaining ; au-delà vous recevez 429 avec Retry-After.

Lister les documents

GET /documents?limit=25&status=approved — documents.read

Les plus récents d'abord. limit accepte 1–100 (25 par défaut) et status filtre sur queued, processing, review, approved, exported ou 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 }
      ]
    }
  ]
}

Les valeurs corrigées remplacent la lecture initiale : value est toujours la valeur à utiliser.

Obtenir un document

GET /documents/{id} — documents.read

Renvoie le document avec sa catégorie, son fournisseur, son montant, sa date et tous ses champs.

curl -X GET "https://api.formalini.com/api/public/v1/documents/b0f1c8d2-5a71-4c0e-9f2a-1d4b6e8c3a90" \
  -H "Authorization: Bearer pf_live_your_key"

Envoyer un fichier et récupérer ses données

POST /documents/upload — documents.write

Envoyez l'image d'une page et recevez les données structurées dans la même réponse. Le document apparaît dans l'atelier comme tout autre scan, consomme un crédit IA par page et déclenche vos webhooks à la fin.

  • file — l'image de la page ; répétez le champ une fois par page (jusqu'à 25) pour un document multipage
  • name — nom de document facultatif ; sinon le nom du fichier est utilisé
  • template_id — identifiant de modèle facultatif, pour retrouver vos propres noms de champs
  • async — mettez true pour recevoir un 202 immédiat et interroger 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 }
    ]
  }
}

JPEG, PNG, WebP et HEIC sont acceptés, jusqu'à 100 Mo par page. La lecture d'une page prend quelques secondes : prévoyez un délai généreux ou utilisez async=true. Un 402 signifie que l'espace n'a plus de crédits, de stockage ou d'allocation quotidienne, et la réponse précise lequel.

Les PDF ne sont pas encore acceptés ici : convertissez chaque page en JPEG ou PNG et envoyez-les en répétant le champ file. L'envoi d'un PDF dans l'application fonctionne toujours, car les pages sont converties dans votre navigateur.

Créer un document

POST /documents — documents.write

Envoyez un document dont vous avez déjà les données. name est obligatoire ; fields est facultatif et accepte jusqu'à 400 entrées. Avec des champs il part en revue, sans champs dans la file.

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

Mettre à jour un document

PATCH /documents/{id} — documents.write

Changez le statut ou corrigez des valeurs par nom. Les statuts approved, exported et review déclenchent aussi le webhook correspondant.

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 }

Déclencher vos webhooks

POST /workflows/dispatch — workflows.run

Envoie un événement à tous les webhooks actifs qui y sont abonnés, pour lancer un autre système à la demande. Événements permis : 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

Ajoutez une URL de webhook dans l'application et choisissez les événements. Chaque envoi est un POST avec un corps JSON et un en-tête de signature, signé avec votre secret.

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": { … } }

Les envois en échec sont réessayés jusqu'à cinq fois avec un délai croissant, et vous pouvez rejouer n'importe quel envoi depuis l'application.

Vérifiez la signature sur le corps brut avant de faire confiance à un envoi :

// 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=", "")));

Erreurs

  • 400 — le corps n'est pas du JSON, ou une valeur obligatoire manque ou est invalide
  • 401 — pas de clé, ou clé révoquée ou expirée
  • 403 — la clé n'a pas le droit exigé par cet endpoint
  • 404 — aucun document avec cet id dans cet espace
  • 429 — limite par minute atteinte — attendez et réessayez
  • 500 — quelque chose a échoué chez nous ; la réponse le précise