# Vision par API : lire un reçu mobile money avec un LLM (JSON)

> Envoie la photo d'un reçu mobile money ou d'une facture à un LLM et récupère du JSON. Base64 ou URL, 3 modèles comparés, crédits et temps mesurés.

- Published: 2026-10-15
- Updated: 2026-11-02
- Author: Gael — fondateur de Tchavi
- Canonical: https://tchavi.com/fr/blog/vision-api-lire-recu-image-llm

---


Pour lire un reçu de mobile money ou une facture de boutique avec un LLM, tu envoies l'image dans le tableau `content` d'un message `user`, à côté de ta consigne, et tu demandes du JSON en retour. Un seul appel `POST /v1/chat/completions`, le même que pour du texte.

J'ai testé trois modèles de vision sur un reçu, une photo de ce reçu et une facture, tous synthétiques. Le résultat qui m'a le plus surpris : le coût par image varie d'un facteur vingt entre les modèles, alors que leurs prix au token sont du même ordre.

## En bref

- **Tchavi, une passerelle d'API IA compatible OpenAI que tu paies en mobile money, accepte les images dans `chat/completions`** : un bloc `{ type: "image_url", image_url: { url } }` dans le tableau `content`, avec l'URL ou une chaîne `data:` en base64.
- **Précision mesurée** : sur un reçu net, une photo dégradée et une facture, `gemini-3.1-flash-lite` et `claude-haiku-5-5` ont tout juste (10/10, 10/10 et 18/18). `gpt-4o-mini` a fait deux erreurs sur la photo et un caractère parasite sur le reçu net.
- **Coût mesuré pour un reçu** : 0,438 crédit avec `claude-haiku-5-5`, 1,139 avec `gemini-3.1-flash-lite`, 8,607 avec `gpt-4o-mini`. Avec 3 crédits pour 1 F, ça fait environ 0,15 F, 0,38 F et 2,9 F.
- **Base64 ou URL : même nombre de tokens** sur la même image, donc même facture. La différence est pratique, pas financière.
- **N'accepte jamais un reçu en photo comme preuve de paiement.** Une image se falsifie : le crédit se donne sur confirmation de l'opérateur, pas sur une capture d'écran.

## Le format : un tableau de blocs

Dans `messages`, le `content` d'un message `user` peut être une chaîne ou un tableau de blocs. Pour une image :

```json
{
  "model": "claude-haiku-5-5",
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "Lis ce reçu et réponds en JSON." },
        { "type": "image_url", "image_url": { "url": "https://exemple.com/recu.png" } }
      ]
    }
  ]
}
```

C'est le format décrit dans la [documentation de `chat/completions`](https://tchavi.com/fr/docs/chat-completions), avec les blocs `text`, `image_url` et `document_url` (pour les PDF). Tous les modèles ne lisent pas les images : vérifie la capacité sur la page du modèle ou dans `GET /v1/models`. J'ai utilisé [`gpt-4o-mini`](https://tchavi.com/fr/m/openai/gpt-4o-mini), [`gemini-3.1-flash-lite`](https://tchavi.com/fr/m/google/gemini-3.1-flash-lite) et [`claude-haiku-5-5`](https://tchavi.com/fr/m/anthropic/claude-haiku-5-5).

## Base64 ou URL

Tu as deux façons de donner l'image :

- **Une URL publique** : pratique si l'image est déjà hébergée (ton stockage, ton CDN). Le fournisseur doit pouvoir la télécharger : une adresse locale, privée ou derrière une connexion ne marchera pas.
- **Du base64 dans une chaîne `data:`** : c'est ce qu'il te faut pour un reçu que ton client vient d'envoyer sur WhatsApp ou depuis ton app. Rien à héberger, mais la requête grossit d'environ un tiers de la taille du fichier.

Je les ai comparées sur la même image, un petit logo public de 192 x 75 pixels, avec deux modèles :

| Modèle             | Transport | Tokens d'entrée |
| ------------------ | --------- | --------------- |
| `gpt-4o-mini`      | URL       | 8 515           |
| `gpt-4o-mini`      | base64    | 8 515           |
| `claude-haiku-5-5` | URL       | 44              |
| `claude-haiku-5-5` | base64    | 44              |

Les tokens sont les mêmes, donc le coût de l'image aussi. Les écarts de crédits entre mes deux appels viennent de la longueur de la réponse et, pour `gpt-4o-mini`, d'une partie de l'entrée servie depuis le cache (2 944 tokens la deuxième fois). Les temps étaient proches (1,8 s contre 1,1 s, et 2,5 s contre 1,6 s : trop peu d'essais pour conclure quoi que ce soit). Je n'ai pas hébergé mes reçus synthétiques : le test d'URL porte sur le logo, et les reçus sont partis en base64.

## Le code, en Node.js

````javascript
import { readFileSync } from 'node:fs';

const BASE = 'https://tchavi.com/api/v1';

const CONSIGNE =
  'Lis ce reçu de mobile money. Réponds uniquement avec un objet JSON : ' +
  '{"operateur": string, "montant": number, "devise": string, "frais": number, ' +
  '"destinataire": string, "numero": string, "reference": string, ' +
  '"date": "AAAA-MM-JJ", "heure": "HH:MM", "solde": number}. ' +
  'Les montants sont des nombres sans espaces. Si un champ est illisible, mets null.';

export async function lireRecu(fichier, modele = 'claude-haiku-5-5') {
  const mime = fichier.endsWith('.jpg') ? 'image/jpeg' : 'image/png';
  const dataUrl = `data:${mime};base64,${readFileSync(fichier).toString('base64')}`;

  const res = await fetch(`${BASE}/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${process.env.TCHAVI_API_KEY}`,
    },
    body: JSON.stringify({
      model: modele,
      temperature: 0,
      max_tokens: 800,
      messages: [
        {
          role: 'user',
          content: [
            { type: 'text', text: CONSIGNE },
            { type: 'image_url', image_url: { url: dataUrl } },
          ],
        },
      ],
    }),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error} : ${json.message}`);

  // Certains modèles entourent le JSON de ```json … ``` : on l'enlève.
  const brut = json.choices[0].message.content.replace(/^```(?:json)?\s*|\s*```$/g, '');
  return {
    donnees: JSON.parse(brut),
    credits: Number(res.headers.get('x-credits-used')),
  };
}
````

## Le même appel en Python

````python
import base64
import json
import os
import re

import requests

CONSIGNE = (
    "Lis ce reçu de mobile money. Réponds uniquement avec un objet JSON : "
    '{"operateur": string, "montant": number, "devise": string, "frais": number, '
    '"destinataire": string, "numero": string, "reference": string, '
    '"date": "AAAA-MM-JJ", "heure": "HH:MM", "solde": number}. '
    "Les montants sont des nombres sans espaces. Si un champ est illisible, mets null."
)


def lire_recu(fichier, modele="claude-haiku-5-5"):
    mime = "image/jpeg" if fichier.endswith(".jpg") else "image/png"
    b64 = base64.b64encode(open(fichier, "rb").read()).decode()
    r = requests.post(
        "https://tchavi.com/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['TCHAVI_API_KEY']}"},
        json={
            "model": modele,
            "temperature": 0,
            "max_tokens": 800,
            "messages": [{
                "role": "user",
                "content": [
                    {"type": "text", "text": CONSIGNE},
                    {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}},
                ],
            }],
        },
        timeout=120,
    )
    corps = r.json()
    if not r.ok:
        raise RuntimeError(f"{corps['error']} : {corps['message']}")
    texte = re.sub(r"^```(?:json)?\s*|\s*```$", "", corps["choices"][0]["message"]["content"])
    return json.loads(texte), float(r.headers.get("x-credits-used", 0))
````

Un point de vigilance : je n'ai utilisé `temperature: 0` que sur mes trois modèles. Si un autre modèle, un modèle de raisonnement par exemple, te renvoie une erreur à cause de ce paramètre, retire-le.

Je n'ai pas testé l'option `response_format` (`json_object` ou `json_schema`) dans cette série, donc je ne dis pas qu'elle marche sur chaque modèle : sa prise en charge varie, l'onglet API du modèle te le dit. En attendant, la consigne qui impose le JSON et le nettoyage des blocs de code ci-dessus ont suffi sur mes trois modèles.

## Le test : un reçu fictif, une photo, une facture

Pour ne montrer les données de personne, j'ai fabriqué trois images avec Python (PIL), toutes marquées « DOCUMENT FICTIF » :

1. un reçu de transfert, net, avec dix champs ;
2. le même reçu « photographié » : perspective, flou, ombre, bruit, JPEG compressé ;
3. une facture de boutique légèrement penchée, avec trois lignes, un total et une référence de paiement (18 valeurs).

Les noms, numéros et références sont inventés (« Boutique Awa Démo », « TXN-7Q4K92LM18 », 15 000 FCFA). Une seule exécution par modèle et par image, avec `temperature: 0` : c'est un petit échantillon, pas un banc d'essai.

### Précision, champ par champ

| Image                     | `gpt-4o-mini` | `gemini-3.1-flash-lite` | `claude-haiku-5-5` |
| ------------------------- | ------------- | ----------------------- | ------------------ |
| Reçu net (10 champs)      | 9/10          | 10/10                   | 10/10              |
| Reçu en photo (10 champs) | 8/10          | 10/10                   | 10/10              |
| Facture (18 valeurs)      | 18/18         | 18/18                   | 18/18              |

Le détail des erreurs de `gpt-4o-mini` :

- **Reçu net** : le champ `destinataire` est revenu avec un `о` cyrillique à la place du `o` latin dans « Démo ». À l'œil, c'est identique. Dans une base de données, c'est un autre texte, et une recherche exacte échoue. Je l'ai trouvé en comparant les codes des caractères.
- **Reçu en photo** : `destinataire` coupé en « Boutique Awa Dém », et `numero` rendu `+22901000000`, avec deux zéros en moins. Un numéro faux est pire qu'un champ vide.

Les montants, la date, l'heure, la référence et l'opérateur étaient justes chez les trois modèles sur les trois images. L'erreur est venue des textes longs et des numéros, pas des montants. Je ne tire pas de loi de ça sur d'autres reçus : teste avec les tiens.

### Crédits et temps

| Image         | `gpt-4o-mini`   | `gemini-3.1-flash-lite` | `claude-haiku-5-5` |
| ------------- | --------------- | ----------------------- | ------------------ |
| Reçu net      | 8,607 cr, 2,6 s | 1,139 cr, 1,9 s         | 0,438 cr, 1,9 s    |
| Reçu en photo | 8,596 cr, 2,9 s | 1,114 cr, 1,8 s         | 0,678 cr, 4,0 s    |
| Facture       | 8,723 cr, 3,5 s | 1,593 cr, 2,3 s         | 0,478 cr, 2,4 s    |

Pourquoi un tel écart ? Regarde les tokens d'entrée du reçu net (640 x 980 pixels) :

| Modèle                  | Tokens d'entrée | Prix d'entrée (crédits / 1 000 tokens) |
| ----------------------- | --------------- | -------------------------------------- |
| `gpt-4o-mini`           | 25 607          | 0,33                                   |
| `gemini-3.1-flash-lite` | 1 205           | 0,581                                  |
| `claude-haiku-5-5`      | 957             | 0,22                                   |

`gpt-4o-mini` a le deuxième prix au token, mais il compte la même image comme plus de vingt fois plus de tokens : la facture suit. Le prix par token seul ne dit donc pas combien coûtera une image. Compare sur une vraie image, avec `x-credits-used`.

Les coûts se recomptent : pour `claude-haiku-5-5` sur le reçu net, 957 tokens en entrée à 0,22 et 206 en sortie à 1,1 pour 1 000 donnent 0,437, et j'ai mesuré 0,438. À 0,438 crédit le reçu, mille reçus font 438 crédits, soit environ 146 F. Avec `gpt-4o-mini`, 8 607 crédits, soit environ 2 869 F. Ce sont les tarifs publics ; certains modèles bénéficient en ce moment d'une remise, et la page de chaque modèle affiche le prix facturé du moment.

## Ce que je ferais en production

1. **Choisis le modèle sur tes images**, avec trente reçus réels (en retirant les données personnelles) et le décompte champ par champ ci-dessus.
2. **Valide le JSON avec du code** : le montant est un nombre positif, la date existe, la référence suit le format de ton opérateur, et sur une facture la somme des lignes égale le total. Mes trois modèles ont tous passé ce dernier contrôle, mais c'est lui qui rattrapera l'erreur un jour.
3. **Normalise les textes** (Unicode, espaces) avant de comparer ou de stocker, à cause du caractère cyrillique de mon test.
4. **Demande `null` plutôt qu'une supposition** : la consigne « si un champ est illisible, mets null » est dans mon code pour ça.
5. **Ne considère pas l'image comme une preuve de paiement.** Pour créditer un client, attends le webhook ou la confirmation de l'opérateur. Le reçu lu par le LLM sert à pré-remplir un formulaire, pas à valider un paiement.
6. **Plafonne la taille des images** que tu acceptes et réduis-les avant l'envoi : le nombre de tokens suit les dimensions.

Pour les prix du texte en FCFA, tu as [le comparatif des prix](https://tchavi.com/fr/blog/prix-api-ia-fcfa-2026).

## FAQ

### Comment envoyer une image à un LLM par l'API ?

Dans `messages`, mets un message `user` dont le `content` est un tableau avec un bloc `text` et un bloc `image_url`. L'URL peut être une adresse publique ou une chaîne `data:image/png;base64,...`.

### Base64 ou URL : lequel choisir ?

Le base64 pour une image que tu viens de recevoir et que tu n'as pas hébergée, l'URL pour une image déjà en ligne et publique. Sur la même image, j'ai mesuré les mêmes tokens avec les deux, donc le même coût.

### Quels modèles lisent les images ?

Ceux dont la capacité vision est affichée sur leur page ou dans `GET /v1/models`. J'ai testé `gpt-4o-mini`, `gemini-3.1-flash-lite` et `claude-haiku-5-5`.

### Combien coûte la lecture d'un reçu ?

Entre 0,438 et 8,607 crédits sur mes trois modèles pour un reçu de 640 x 980 pixels, soit de 0,15 F à 2,9 F. Le coût dépend surtout du nombre de tokens que chaque modèle attribue à l'image.

### Le LLM peut-il remplacer un OCR ?

Pour des reçus et des factures simples, il a bien fait sur mes essais, y compris sur une photo dégradée. Il peut aussi se tromper de façon discrète, comme un numéro avec des chiffres en moins. Garde donc des contrôles dans ton code.

### Puis-je lire un PDF ?

Oui, avec un bloc `document_url`, sur les modèles qui ont la capacité documents (par exemple les modèles Claude). Je ne l'ai pas testé ici.

### Comment payer sans carte bancaire ?

En mobile money : tu recharges à partir de 1 000 F, 3 crédits valent 1 F, et il n'y a ni abonnement ni crédits gratuits à l'inscription. Ta première recharge ajoute 150 crédits de bonus.

[crée ton compte et recharge 1 000 F](https://tchavi.com/fr/register?next=%2Fdashboard%2Fbilling%3Famount%3D1000%26currency%3DXOF%26from%3Dblog-vision)

> _Cet article fait partie de la série « Construire un SaaS IA depuis l'Afrique francophone ». Tchavi est une passerelle d'API IA compatible OpenAI : une seule clé donne accès à plus de 100 modèles de texte, d'image, d'audio et de vidéo, et tu la paies en mobile money, sans carte bancaire ([pays et opérateurs](https://tchavi.com/fr/docs/payments)). Plus de guides sur [le blog](https://tchavi.com/fr/blog). Pour suivre les prochains : [LinkedIn](https://www.linkedin.com/company/tchavi) et [X](https://x.com/tchaviai)._

