HttpClient + MultipartFormDataContent, idéal pour pipelines finance/ERP.

Intégrer FacturX API avec .NET

Fonctionne pour ASP.NET Core, Azure Functions ou Windows Services.

Support Polly pour retry/resilience
Compatible Azure WebJobs / Functions
Parfait pour intégrateurs ERP

Exemple cURL /convert

multipart/form-data — /convert + invoice_data
FACTURX_OUTPUT='facturx_pdfa3'
FACTURX_VALIDATION_TARGET='en16931'
FACTURX_INVOICE_DATA='{"invoice_number":"FA-2026-042","issue_date":"2026-04-01","invoice_type":"380","currency":"EUR","seller":{"name":"Ma Societe SAS","siret":"10000000900017","vat_id":"FR88100000009","address":{"street":"10 rue de Rivoli","city":"Paris","postal_code":"75001","country":"FR"}},"buyer":{"name":"Client SA","siret":"10000001700010","address":{"street":"20 rue de la Republique","city":"Lyon","postal_code":"69002","country":"FR"}},"references":{"buyer_reference":"SERVICE-ACHATS","purchase_order_reference":"PO-2026-0017","contract_reference":"CTR-2026-04"},"totals":{"net":"970.00","tax":"194.00","gross":"1164.00","prepaid_amount":"100.00","rounding_amount":"0.00","due":"1064.00"},"tax_breakdown":[{"rate":"20.00","category":"S","base":"970.00","amount":"194.00"}],"line_items":[{"number":"1","description":"Prestation conseil","quantity":"10","unit":"C62","unit_price":"100.00","net_amount":"970.00","vat_rate":"20.00","vat_category":"S","purchase_order_line_reference":"10","gross_unit_price":"120.00","price_discount":"20.00","allowances":[{"amount":"50.00","reason":"Remise ligne","reason_code":"95"}],"charges":[{"amount":"20.00","reason":"Supplement urgent"}]}],"payment":{"due_date":"2026-05-01","terms":"Paiement a 30 jours","iban":"FR7630006000011234567890189"},"delivery":{"date":"2026-04-01","address":{"street":"20 rue de la Republique","city":"Lyon","postal_code":"69002","country":"FR"}},"invoicing_period":{"start_date":"2026-04-01","end_date":"2026-04-30"}}'
FACTURX_OPERATION_IDENTITY=$(printf '%s\0%s\0%s'   "$FACTURX_OUTPUT" "$FACTURX_VALIDATION_TARGET" "$FACTURX_INVOICE_DATA"   | openssl dgst -sha256 -r | awk '{print $1}')
FACTURX_IDEMPOTENCY_KEY="convert-$FACTURX_OPERATION_IDENTITY"

curl -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer $FACTURX_API_KEY" \
  -H "Accept-Language: fr" \
  -H "Idempotency-Key: $FACTURX_IDEMPOTENCY_KEY" \
  -F "output=$FACTURX_OUTPUT" \
  -F "validation_target=$FACTURX_VALIDATION_TARGET" \
  -F "invoice_data=$FACTURX_INVOICE_DATA"

Snippet .NET

HttpClient — génération /api/v1/convert
using System.Globalization;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;

var apiKey = Environment.GetEnvironmentVariable("FACTURX_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
    throw new InvalidOperationException("Définissez FACTURX_API_KEY avant de lancer cet exemple.");
}

var apiBaseUrl = Environment.GetEnvironmentVariable("FACTURX_API_BASE_URL")
    ?? "https://api.facturxapi.com";
var outputPath = Environment.GetEnvironmentVariable("FACTURX_OUTPUT_PATH")
    ?? "facture-facturx.pdf";
var explicitIdempotencyKey = Environment.GetEnvironmentVariable("FACTURX_IDEMPOTENCY_KEY");

var invoiceData = """
{
  "invoice_number": "FA-2026-042",
  "issue_date": "2026-04-01",
  "invoice_type": "380",
  "currency": "EUR",
  "seller": {
    "name": "Ma Societe SAS",
    "siret": "10000000900017",
    "vat_id": "FR88100000009",
    "address": {
      "street": "10 rue de Rivoli",
      "city": "Paris",
      "postal_code": "75001",
      "country": "FR"
    }
  },
  "buyer": {
    "name": "Client SA",
    "siret": "10000001700010",
    "address": {
      "street": "20 rue de la Republique",
      "city": "Lyon",
      "postal_code": "69002",
      "country": "FR"
    }
  },
  "references": {
    "buyer_reference": "SERVICE-ACHATS",
    "purchase_order_reference": "PO-2026-0017",
    "contract_reference": "CTR-2026-04"
  },
  "totals": {
    "net": "970.00",
    "tax": "194.00",
    "gross": "1164.00",
    "prepaid_amount": "100.00",
    "rounding_amount": "0.00",
    "due": "1064.00"
  },
  "tax_breakdown": [
    {
      "rate": "20.00",
      "category": "S",
      "base": "970.00",
      "amount": "194.00"
    }
  ],
  "line_items": [
    {
      "number": "1",
      "description": "Prestation conseil",
      "quantity": "10",
      "unit": "C62",
      "unit_price": "100.00",
      "net_amount": "970.00",
      "vat_rate": "20.00",
      "vat_category": "S",
      "purchase_order_line_reference": "10",
      "gross_unit_price": "120.00",
      "price_discount": "20.00",
      "allowances": [
        {
          "amount": "50.00",
          "reason": "Remise ligne",
          "reason_code": "95"
        }
      ],
      "charges": [
        {
          "amount": "20.00",
          "reason": "Supplement urgent"
        }
      ]
    }
  ],
  "payment": {
    "due_date": "2026-05-01",
    "terms": "Paiement a 30 jours",
    "iban": "FR7630006000011234567890189"
  },
  "delivery": {
    "date": "2026-04-01",
    "address": {
      "street": "20 rue de la Republique",
      "city": "Lyon",
      "postal_code": "69002",
      "country": "FR"
    }
  },
  "invoicing_period": {
    "start_date": "2026-04-01",
    "end_date": "2026-04-30"
  }
}
""";

using var client = new HttpClient
{
    BaseAddress = new Uri(apiBaseUrl, UriKind.Absolute),
    Timeout = TimeSpan.FromSeconds(30)
};
client.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", apiKey);
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("fr");
var requestIdentity = SHA256.HashData(Encoding.UTF8.GetBytes(
    $"facturx_pdfa3\nen16931\n{invoiceData}"
));
var idempotencyKey = string.IsNullOrWhiteSpace(explicitIdempotencyKey)
    ? $"dotnet-{Convert.ToHexString(requestIdentity).ToLowerInvariant()}"
    : explicitIdempotencyKey;
client.DefaultRequestHeaders.Add("Idempotency-Key", idempotencyKey);

using var form = new MultipartFormDataContent();
form.Add(new StringContent("facturx_pdfa3"), "output");
form.Add(new StringContent("en16931"), "validation_target");
form.Add(new StringContent(invoiceData, Encoding.UTF8, "application/json"), "invoice_data");

using var response = await client.PostAsync("/api/v1/convert", form);
var responseBody = await response.Content.ReadAsStringAsync();

if (!response.IsSuccessStatusCode)
{
    var requestId = response.Headers.TryGetValues("X-Request-Id", out var values)
        ? values.FirstOrDefault()
        : null;
    throw new HttpRequestException(
        $"FacturX API a répondu HTTP {(int)response.StatusCode} (request_id: {requestId ?? "absent"})."
    );
}

using var payload = JsonDocument.Parse(responseBody);
var root = payload.RootElement;
var success = root.GetProperty("success").GetBoolean();
var target = root.GetProperty("target");
var requested = target.GetProperty("requested").GetString();
var executed = target.GetProperty("executed").GetString();
var targetStatus = target.GetProperty("status").GetString();
if (!success || requested != "en16931" || executed != "en16931" || targetStatus != "verified")
{
    throw new InvalidOperationException("La conversion n'atteste pas une cible EN16931 vérifiée.");
}

var result = root.GetProperty("result");
if (!result.GetProperty("packagingPerformed").GetBoolean())
{
    throw new InvalidOperationException("Le packaging PDF/A-3 n'a pas été effectué.");
}

var hasInlinePdf = result.TryGetProperty("pdf", out var pdfElement)
    && pdfElement.ValueKind == JsonValueKind.String
    && !string.IsNullOrWhiteSpace(pdfElement.GetString());
var hasRemotePdf = result.TryGetProperty("pdfUrl", out var pdfUrlElement)
    && pdfUrlElement.ValueKind == JsonValueKind.String
    && !string.IsNullOrWhiteSpace(pdfUrlElement.GetString());
if (hasInlinePdf == hasRemotePdf)
{
    throw new InvalidOperationException("La réponse doit contenir exactement un livrable PDF (pdf ou pdfUrl).");
}

var xmlBase64 = result.GetProperty("xml").GetString();
if (string.IsNullOrWhiteSpace(xmlBase64))
{
    throw new InvalidOperationException("Le XML CII embarqué est absent.");
}
var xmlDocument = XDocument.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(xmlBase64)));
using var sourceInvoice = JsonDocument.Parse(invoiceData);
AssertMaterialXmlMatchesSource(xmlDocument, sourceInvoice.RootElement);

const int maxPdfBytes = 10 * 1024 * 1024;
byte[] pdfBytes;
var deliverableSource = hasInlinePdf ? "inline" : "url";
if (hasInlinePdf)
{
    pdfBytes = Convert.FromBase64String(pdfElement.GetString()!);
}
else
{
    var pdfUrl = new Uri(pdfUrlElement.GetString()!, UriKind.Absolute);
    if (pdfUrl.Scheme != Uri.UriSchemeHttps || !string.IsNullOrEmpty(pdfUrl.UserInfo))
    {
        throw new InvalidOperationException("L'URL du PDF doit être HTTPS et sans identifiants.");
    }
    using var downloadClient = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });
    using var downloadResponse = await downloadClient.GetAsync(pdfUrl);
    downloadResponse.EnsureSuccessStatusCode();
    pdfBytes = await downloadResponse.Content.ReadAsByteArrayAsync();
}

var pdfHeader = Encoding.ASCII.GetString(pdfBytes, 0, Math.Min(8, pdfBytes.Length));
if (pdfBytes.Length <= 0 || pdfBytes.Length > maxPdfBytes
    || !pdfHeader.StartsWith("%PDF-", StringComparison.Ordinal))
{
    throw new InvalidOperationException("Le livrable n'est pas un PDF valide de 10 Mo maximum.");
}
if (result.TryGetProperty("pdfSize", out var pdfSizeElement)
    && pdfSizeElement.TryGetInt64(out var announcedPdfSize)
    && announcedPdfSize != pdfBytes.Length)
{
    throw new InvalidOperationException("La taille du PDF ne correspond pas à la taille annoncée.");
}

var finalPath = Path.GetFullPath(outputPath);
var parentDirectory = Path.GetDirectoryName(finalPath)
    ?? throw new InvalidOperationException("Chemin de sortie PDF invalide.");
if (!Directory.Exists(parentDirectory) || File.Exists(finalPath))
{
    throw new IOException("Le dossier de sortie doit exister et le fichier final ne doit pas déjà exister.");
}
var temporaryPath = Path.Combine(parentDirectory, $".{Path.GetFileName(finalPath)}.{Guid.NewGuid():N}.tmp");
try
{
    await File.WriteAllBytesAsync(temporaryPath, pdfBytes);
    File.Move(temporaryPath, finalPath);
}
finally
{
    if (File.Exists(temporaryPath)) File.Delete(temporaryPath);
}

var requestIdValue = root.TryGetProperty("request_id", out var requestIdElement)
    ? requestIdElement.GetString()
    : null;
if (string.IsNullOrWhiteSpace(requestIdValue))
{
    throw new InvalidOperationException("La réponse vérifiée ne contient pas de request_id traçable.");
}
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine(JsonSerializer.Serialize(new
{
    request_id = requestIdValue,
    success,
    target = new { requested, executed, status = targetStatus },
    deliverable = new
    {
        kind = "pdf",
        source = deliverableSource,
        bytes = pdfBytes.Length,
        saved_file = Path.GetFileName(finalPath)
    },
    material_oracle = new { carrier_source = "invoice_data", xml_fields_matched = 7 }
}, new JsonSerializerOptions { WriteIndented = true }));

static void AssertMaterialXmlMatchesSource(XDocument xml, JsonElement source)
{
    AssertText(XmlUnder(xml, "ExchangedDocument", "ID"), source.GetProperty("invoice_number").GetString(), "invoice_number");
    AssertText(XmlUnder(xml, "SellerTradeParty", "Name"), source.GetProperty("seller").GetProperty("name").GetString(), "seller.name");
    AssertText(XmlUnder(xml, "BuyerTradeParty", "Name"), source.GetProperty("buyer").GetProperty("name").GetString(), "buyer.name");
    AssertText(XmlValue(xml, "InvoiceCurrencyCode"), source.GetProperty("currency").GetString(), "currency");
    AssertText(XmlUnder(xml, "IssueDateTime", "DateTimeString"), source.GetProperty("issue_date").GetString()?.Replace("-", ""), "issue_date");
    AssertMoney(XmlValue(xml, "GrandTotalAmount"), source.GetProperty("totals").GetProperty("gross").GetString(), "totals.gross");
    AssertMoney(XmlValue(xml, "DuePayableAmount"), source.GetProperty("totals").GetProperty("due").GetString(), "totals.due");
}

static string XmlUnder(XDocument xml, string containerName, string valueName)
{
    var container = xml.Descendants().First(element => element.Name.LocalName == containerName);
    return container.Descendants().First(element => element.Name.LocalName == valueName).Value.Trim();
}

static string XmlValue(XDocument xml, string valueName) =>
    xml.Descendants().First(element => element.Name.LocalName == valueName).Value.Trim();

static void AssertText(string actual, string? expected, string field)
{
    if (string.IsNullOrWhiteSpace(expected) || !string.Equals(actual, expected, StringComparison.Ordinal))
        throw new InvalidOperationException($"Le XML généré ne correspond pas à invoice_data pour {field}.");
}

static void AssertMoney(string actual, string? expected, string field)
{
    if (!decimal.TryParse(actual, NumberStyles.Number, CultureInfo.InvariantCulture, out var actualValue)
        || !decimal.TryParse(expected, NumberStyles.Number, CultureInfo.InvariantCulture, out var expectedValue)
        || actualValue != expectedValue)
        throw new InvalidOperationException($"Le XML généré ne correspond pas à invoice_data pour {field}.");
}

Parcours conseillé — générer puis contrôler

Commencez par /convert pour produire un livrable Factur-X contrôlé, puis utilisez /validate comme précontrôle ou contrôle indépendant.

POST /api/v1/convert

PDF → Factur-X PDF/A-3

Génère un Factur-X PDF/A-3 à partir d'un PDF visuel et de données invoice_data. Les livrables ne sont exposés que si la cible demandée est vérifiée.

FACTURX_OUTPUT='facturx_pdfa3'
FACTURX_VALIDATION_TARGET='en16931'
FACTURX_INVOICE_DATA='{"invoice_number":"FA-2026-042","issue_date":"2026-04-01","invoice_type":"380","currency":"EUR","seller":{"name":"Ma Societe SAS","siret":"10000000900017","vat_id":"FR88100000009","address":{"street":"10 rue de Rivoli","city":"Paris","postal_code":"75001","country":"FR"}},"buyer":{"name":"Client SA","siret":"10000001700010","address":{"street":"20 rue de la Republique","city":"Lyon","postal_code":"69002","country":"FR"}},"references":{"buyer_reference":"SERVICE-ACHATS","purchase_order_reference":"PO-2026-0017","contract_reference":"CTR-2026-04"},"totals":{"net":"970.00","tax":"194.00","gross":"1164.00","prepaid_amount":"100.00","rounding_amount":"0.00","due":"1064.00"},"tax_breakdown":[{"rate":"20.00","category":"S","base":"970.00","amount":"194.00"}],"line_items":[{"number":"1","description":"Prestation conseil","quantity":"10","unit":"C62","unit_price":"100.00","net_amount":"970.00","vat_rate":"20.00","vat_category":"S","purchase_order_line_reference":"10","gross_unit_price":"120.00","price_discount":"20.00","allowances":[{"amount":"50.00","reason":"Remise ligne","reason_code":"95"}],"charges":[{"amount":"20.00","reason":"Supplement urgent"}]}],"payment":{"due_date":"2026-05-01","terms":"Paiement a 30 jours","iban":"FR7630006000011234567890189"},"delivery":{"date":"2026-04-01","address":{"street":"20 rue de la Republique","city":"Lyon","postal_code":"69002","country":"FR"}},"invoicing_period":{"start_date":"2026-04-01","end_date":"2026-04-30"}}'
FACTURX_OPERATION_IDENTITY=$(printf '%s\0%s\0%s'   "$FACTURX_OUTPUT" "$FACTURX_VALIDATION_TARGET" "$FACTURX_INVOICE_DATA"   | openssl dgst -sha256 -r | awk '{print $1}')
FACTURX_IDEMPOTENCY_KEY="convert-$FACTURX_OPERATION_IDENTITY"

curl -X POST https://api.facturxapi.com/api/v1/convert \
  -H "Authorization: Bearer $KEY" \
  -H "Accept-Language: fr" \
  -H "Idempotency-Key: $FACTURX_IDEMPOTENCY_KEY" \
  -F "output=$FACTURX_OUTPUT" \
  -F "validation_target=$FACTURX_VALIDATION_TARGET" \
  -F "invoice_data=$FACTURX_INVOICE_DATA"

POST /api/v1/validate

Précontrôle technique

Contrôle un PDF Factur-X ou un XML CII déjà produit. Retourne un rapport JSON structuré avec codes BR-* par ligne/champ.

curl -X POST https://api.facturxapi.com/api/v1/validate?validation_target=en16931 \
  -H "Authorization: Bearer $KEY" \
  -F "file=@./facture.pdf"

POST /api/v1/extract

XML CII embarqué → JSON

Extrait le XML CII d'un PDF Factur-X reçu. Paramètre validate=true combine extraction + validation en une requête.

curl -X POST "https://api.facturxapi.com/api/v1/extract?validate=true&lang=fr" \
  -H "Authorization: Bearer $KEY" \
  -F "file=@./facture-recue.pdf"

POST /api/v1/repair

Corrections déterministes

Corrige automatiquement les erreurs réparables d'un XML CII (dates, décimaux, namespaces, schemeID). Retour : diff + XML réparé.

curl -X POST https://api.facturxapi.com/api/v1/repair \
  -H "Authorization: Bearer $KEY" \
  -F "file=@./facture-invalide.xml"

Checklist intégration

  • Réutiliser HttpClient (DI) pour éviter l'épuisement des sockets

  • Ajouter Polly (WaitAndRetry) sur 429/502

  • Logguer request_id via ILogger pour support

Retries & timeouts

Policy de retry Polly : 3 tentatives avec jitter + circuit-breaker sur 5xx consécutifs.

  • • Timeout HttpClient : 15 s
  • • Déclencher une alerte après 3 erreurs Schematron critiques
  • • Purger les fichiers temporaires après validation

Erreurs fréquentes

Mappez vos logs sans confondre codes API publics, exceptions client locales et règles métier EN16931.

Exception client locale

Exception client HTTP

Proxy d'entreprise bloquant l'appel.

💡 Autoriser api.facturxapi.com dans vos règles sortantes ou configurer HttpClientHandler côté .NET.

Code API public

415 unsupported_media_type

Fichier XML/PDF non accepté par l’API.

💡 Convertir le XML en UTF-8 et envoyer le fichier en multipart.

Règle EN16931

BR-CO-26

Aucun identifiant vendeur (BT-29, BT-30 ou BT-31) présent dans la facture.

💡 Renseigner le SIREN dans BT-30 (schemeID="0002") ou le numéro de TVA dans BT-31.

FAQ

Peut-on l'utiliser dans Dynamics ?

Oui, créez un plugin qui envoie le PDF généré via HttpClient.

Comment suivre les quotas ?

Exposez les métriques dans Application Insights via custom dimensions.

Guide PDF/A-3

Vérifier vos exports ERP

Next steps

Activer votre clé

Plan Launch

Tous les endpoints + ERP-assisted