Gestion des données6 min

Envoi de fichiers

Envoyer les fichiers de factures, d'avoirs et de pièces jointes d'email, en binaire ou en base64.

Créer une facture ou un avoir par l’API Billabex suppose de joindre un document, PDF ou image. Ce guide décrit les formats acceptés, les limites de taille et les modes d’encodage.

Vue d’ensemble

Un fichier est exigé par :

  • POST /api/public/v1/invoices : créer une facture
  • POST /api/public/v1/credit-notes : créer un avoir
  • PUT /api/public/v1/organizations/{organizationId}/logo : poser le logo de l’organisation
  • PUT /api/public/v1/users/me/avatar : poser la photo de profil de l’utilisateur authentifié

Ces endpoints acceptent des requêtes multipart/form-data avec un fichier joint.

Les pièces jointes d’email prennent une autre forme : elles voyagent en JSON et non en multipart/form-data, parce qu’un email en porte plusieurs à la fois. Les deux endpoints ci-dessous acceptent un tableau files d’objets { content, filename, mimeType }, où content est le fichier encodé en base64, aux côtés d’un tableau attachmentIds/attachments nommant des fichiers qui existent déjà :

  • POST /api/public/v1/accounts/{accountId}/outgoing-email-communications : envoyer un email tout de suite
  • PUT /api/public/v1/outgoing-email-communications/{communicationId} : modifier une relance planifiée

À la modification, attachments est la liste des identifiants de pièces à conserver : envoyer une liste plus courte est donc la façon de détacher une pièce. Elle n’accepte que des identifiants que le serveur a déjà validés sur cette communication : les fichiers déjà présents sur l’email, ceux téléversés par files dans le même appel, ceux de son manifeste financier et son document de coordonnées bancaires. Un document financier que la relance a déclaré obligatoire ne peut pas être retiré, et requiredAttachmentIds sur la communication dit lesquels le sont. Les fichiers envoyés par files sont attachés que attachments les cite ou non : leurs identifiants sont justement créés par cet appel. Le serveur vérifie l’accès à la relance planifiée avant le téléversement, et supprime tous les fichiers créés par la requête si la modification est refusée.

Les documents créés ainsi portent le canal PublicApi, renvoyé sur chaque facture et chaque avoir. Ils coexistent avec les documents importés par un connecteur et ceux saisis dans la webapp : une organisation peut utiliser tout cela en même temps. Voyez Sources de compte pour la différence entre le channel d’un document et la source d’un compte.

Formats acceptés

Type MIME Extension Description
application/pdf .pdf Documents PDF (recommandé)
image/png .png Images PNG
image/jpeg .jpg, .jpeg Images JPEG
image/webp .webp Images WebP

Le logo d’organisation et l’avatar utilisateur font exception : ils n’acceptent que des images (image/png, image/jpeg, image/webp), jamais un PDF, et sont plafonnés à 2 Mo. Un DELETE sur la même route retire l’image. Retirer le logo d’une organisation fait revenir au logo déduit de son domaine email ; retirer un avatar fait revenir aux initiales de l’utilisateur.

Limites de taille

  • Taille maximale : 10 Mo une fois décodé, 2 Mo pour les logos et les avatars
  • Au-delà, le fichier est rejeté avec une erreur 400 Bad Request

Modes d’encodage

Billabex accepte deux encodages :

Encodage Content-Transfer-Encoding Quand l’utiliser
Binaire 7bit, 8bit ou binary Par défaut, pour la plupart des clients HTTP
Base64 base64 Systèmes historiques, NetSuite, etc.

Envoi binaire (par défaut)

La plupart des clients HTTP envoient les fichiers en binaire par défaut. C’est la méthode recommandée quand votre plateforme le permet.

Exemple JavaScript

const formData = new FormData();
formData.append('accountId', 'ACCOUNT_ID');
formData.append('number', 'INV-2024-001');
formData.append('issuedDate.year', '2024');
formData.append('issuedDate.month', '1');
formData.append('issuedDate.day', '15');
formData.append('dueDate.year', '2024');
formData.append('dueDate.month', '2');
formData.append('dueDate.day', '15');
formData.append('totalAmount', '1500.00');
formData.append('taxAmount', '300.00');
formData.append('paidAmount', '0');
formData.append('billingAddress.street', '123 rue Principale');
formData.append('billingAddress.city', 'Paris');
formData.append('billingAddress.postalCode', '75001');
formData.append('billingAddress.country', 'FR');
formData.append('file', fileBlob, 'facture.pdf');

const response = await fetch('[baseURL]/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
  body: formData,
});

Exemple cURL

curl -X POST "[baseURL]/api/public/v1/invoices" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -F "accountId=ACCOUNT_ID" \
  -F "number=INV-2024-001" \
  -F "issuedDate.year=2024" \
  -F "issuedDate.month=1" \
  -F "issuedDate.day=15" \
  -F "dueDate.year=2024" \
  -F "dueDate.month=2" \
  -F "dueDate.day=15" \
  -F "totalAmount=1500.00" \
  -F "taxAmount=300.00" \
  -F "paidAmount=0" \
  -F "billingAddress.street=123 rue Principale" \
  -F "billingAddress.city=Paris" \
  -F "billingAddress.postalCode=75001" \
  -F "billingAddress.country=FR" \
  -F "file=@facture.pdf"

Envoi en base64

Certaines plateformes, NetSuite par exemple, ne savent pas envoyer de binaire et doivent encoder les fichiers en base64. Billabex détecte et décode automatiquement ce cas.

Pour l’utiliser :

  1. encodez le contenu du fichier en base64 ;
  2. posez l’en-tête Content-Transfer-Encoding: base64 sur la partie du fichier.

Ce que fait la base64

L’encodage base64 convertit du binaire en texte ASCII, ce qui augmente la taille d’environ 33 %. Un fichier de 10 Mo pèse environ 13,3 Mo une fois encodé.

Billabex le gère seul :

  • la limite de 10 Mo porte sur la taille décodée ;
  • un envoi encodé jusqu’à environ 13,7 Mo est accepté ;
  • le fichier est décodé côté serveur avant stockage.

Exemple JavaScript (base64)

// Lire le fichier et l'encoder en base64
const fileBuffer = await file.arrayBuffer();
const base64Content = btoa(String.fromCharCode(...new Uint8Array(fileBuffer)));

// Construire la requête multipart avec le fichier encodé
const boundary = '----WebKitFormBoundary' + Math.random().toString(36).slice(2);

const body = [
  `--${boundary}`,
  'Content-Disposition: form-data; name="accountId"',
  '',
  'ACCOUNT_ID',
  `--${boundary}`,
  'Content-Disposition: form-data; name="number"',
  '',
  'INV-2024-001',
  // ... autres champs ...
  `--${boundary}`,
  'Content-Disposition: form-data; name="file"; filename="facture.pdf"',
  'Content-Type: application/pdf',
  'Content-Transfer-Encoding: base64',
  '',
  base64Content,
  `--${boundary}--`,
].join('\r\n');

const response = await fetch('[baseURL]/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': `multipart/form-data; boundary=${boundary}`,
  },
  body: body,
});

Exemple Node.js (base64)

const fs = require('fs');
const path = require('path');

// Lire le fichier et l'encoder en base64
const filePath = './facture.pdf';
const fileContent = fs.readFileSync(filePath);
const base64Content = fileContent.toString('base64');
const filename = path.basename(filePath);

// Construire le corps multipart à la main
const boundary = '----FormBoundary' + Date.now();
const CRLF = '\r\n';

const parts = [
  `--${boundary}`,
  'Content-Disposition: form-data; name="accountId"',
  '',
  'ACCOUNT_ID',
  `--${boundary}`,
  'Content-Disposition: form-data; name="number"',
  '',
  'INV-2024-001',
  `--${boundary}`,
  'Content-Disposition: form-data; name="issuedDate.year"',
  '',
  '2024',
  `--${boundary}`,
  'Content-Disposition: form-data; name="issuedDate.month"',
  '',
  '1',
  `--${boundary}`,
  'Content-Disposition: form-data; name="issuedDate.day"',
  '',
  '15',
  `--${boundary}`,
  'Content-Disposition: form-data; name="dueDate.year"',
  '',
  '2024',
  `--${boundary}`,
  'Content-Disposition: form-data; name="dueDate.month"',
  '',
  '2',
  `--${boundary}`,
  'Content-Disposition: form-data; name="dueDate.day"',
  '',
  '15',
  `--${boundary}`,
  'Content-Disposition: form-data; name="totalAmount"',
  '',
  '1500.00',
  `--${boundary}`,
  'Content-Disposition: form-data; name="taxAmount"',
  '',
  '300.00',
  `--${boundary}`,
  'Content-Disposition: form-data; name="paidAmount"',
  '',
  '0',
  `--${boundary}`,
  'Content-Disposition: form-data; name="billingAddress.street"',
  '',
  '123 rue Principale',
  `--${boundary}`,
  'Content-Disposition: form-data; name="billingAddress.city"',
  '',
  'Paris',
  `--${boundary}`,
  'Content-Disposition: form-data; name="billingAddress.postalCode"',
  '',
  '75001',
  `--${boundary}`,
  'Content-Disposition: form-data; name="billingAddress.country"',
  '',
  'FR',
  `--${boundary}`,
  `Content-Disposition: form-data; name="file"; filename="${filename}"`,
  'Content-Type: application/pdf',
  'Content-Transfer-Encoding: base64',
  '',
  base64Content,
  `--${boundary}--`,
];

const body = parts.join(CRLF);

const response = await fetch('[baseURL]/api/public/v1/invoices', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': `multipart/form-data; boundary=${boundary}`,
  },
  body: body,
});

Exemple Python (base64)

import base64
import requests

# Lire et encoder le fichier
with open('facture.pdf', 'rb') as f:
    file_content = f.read()
    base64_content = base64.b64encode(file_content).decode('ascii')

# Construire le corps multipart
boundary = '----PythonFormBoundary'
crlf = '\r\n'

body_parts = [
    f'--{boundary}',
    'Content-Disposition: form-data; name="accountId"',
    '',
    'ACCOUNT_ID',
    f'--{boundary}',
    'Content-Disposition: form-data; name="number"',
    '',
    'INV-2024-001',
    # ... autres champs ...
    f'--{boundary}',
    'Content-Disposition: form-data; name="file"; filename="facture.pdf"',
    'Content-Type: application/pdf',
    'Content-Transfer-Encoding: base64',
    '',
    base64_content,
    f'--{boundary}--',
]

body = crlf.join(body_parts)

response = requests.post(
    '[baseURL]/api/public/v1/invoices',
    headers={
        'Authorization': f'Bearer {access_token}',
        'Content-Type': f'multipart/form-data; boundary={boundary}',
    },
    data=body.encode('utf-8'),
)

Gestion des erreurs

Erreur Statut HTTP Description
Aucun fichier envoyé 400 Le champ file manque dans la requête
Fichier trop volumineux 400 Le fichier dépasse 10 Mo une fois décodé
Type de fichier invalide 400 Type MIME absent de la liste autorisée
Multipart invalide 400 Requête multipart mal formée

Exemple de réponse d’erreur :

{
  "statusCode": 400,
  "message": "File too large. Maximum size: 10485760 bytes",
  "error": "Bad Request"
}

Bonnes pratiques

Choisir le bon encodage

  • Préférez le binaire dès que possible, il est plus efficace
  • N’utilisez la base64 que si votre plateforme ne sait pas envoyer de binaire

Valider côté client

Vérifiez la taille et le type avant d’envoyer, pour éviter des appels inutiles :

const MAX_SIZE = 10 * 1024 * 1024; // 10 Mo
const ALLOWED_TYPES = [
  'application/pdf',
  'image/png',
  'image/jpeg',
  'image/webp',
];

if (file.size > MAX_SIZE) {
  throw new Error('Fichier trop volumineux');
}

if (!ALLOWED_TYPES.includes(file.type)) {
  throw new Error('Type de fichier invalide');
}

Préférer le PDF

C’est le format recommandé pour un document de facture :

  • il préserve la mise en forme d’un appareil à l’autre ;
  • il permet l’extraction du texte ;
  • il donne des fichiers plus légers sur les documents à dominante textuelle.

Pour aller plus loin

Support

Une question sur l’envoi de fichiers ? Écrivez-nous via le formulaire de contact.