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.
HttpClient + MultipartFormDataContent, idéal pour pipelines finance/ERP.
Fonctionne pour ASP.NET Core, Azure Functions ou Windows Services.
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" 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}.");
} Commencez par /convert pour produire un livrable Factur-X contrôlé, puis utilisez /validate comme précontrôle ou contrôle indépendant.
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" 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" 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" 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" Réutiliser HttpClient (DI) pour éviter l'épuisement des sockets
Ajouter Polly (WaitAndRetry) sur 429/502
Logguer request_id via ILogger pour support
Policy de retry Polly : 3 tentatives avec jitter + circuit-breaker sur 5xx consécutifs.
Mappez vos logs sans confondre codes API publics, exceptions client locales et règles métier EN16931.
Exception client locale
Proxy d'entreprise bloquant l'appel.
💡 Autoriser api.facturxapi.com dans vos règles sortantes ou configurer HttpClientHandler côté .NET.
Code API public
Fichier XML/PDF non accepté par l’API.
💡 Convertir le XML en UTF-8 et envoyer le fichier en multipart.
Règle EN16931
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.
Oui, créez un plugin qui envoie le PDF généré via HttpClient.
Exposez les métriques dans Application Insights via custom dimensions.
Vérifier vos exports ERP
Activer votre clé
Tous les endpoints + ERP-assisted
Et après ?
Choisissez l'étape suivante pour activer FacturX API sur votre stack. Tous les liens sont internes pour garder le suivi clair.
Clé Sandbox par email + clés distinctes pour dev/staging.
Comparer les offres actuelles : Sandbox, Launch, Scale, Business et Pilote accompagné.
Endpoints détaillés + schémas JSON pour parser les réponses.
Déposer un fichier depuis l'UI pour comparer le diagnostic avec la réponse API.