Praxisanleitung von Rechnungs-JSON und Schema-Response bis zu XML, PDF/A-3 und abschließender Validierung.
Der eingesetzte Mikro-Webdienst speichert keine Rechnungsdaten; sie werden ausschließlich im flüchtigen RAM-Arbeitsspeicher verarbeitet. Die Nicht-Speicherung der Rechnungsdaten wird vertraglich zugesichert.
Das folgende Muster erzeugt aus FileMaker-Rechnungsdaten ein strukturiertes Rechnungs-XML und optional aus XML und Sicht-PDF eine hybride PDF/A-3-Datei. Die erzeugte Rechnung muss vor dem Versand abschließend validiert werden.
Alle Codebeispiele verwenden englische FileMaker-Scriptschritte und -Optionen. Tabellenauftreten, Feldnamen und sichtbare Dialogtexte bleiben lösungsspezifisch.
Verify SSL Certificates.
FileMaker-Daten
-> Rechnungs-JSON
-> API-Aufruf 1
<- Rechnungs-XML
-> XML + Sicht-PDF als Base64
-> API-Aufruf 2
<- PDF/A-3 als Base64
-> abschließende Validierung
| Feld | Typ | Verwendung |
|---|---|---|
Rechnungen::ErstellungsRequest | Text | optional: gesendetes Rechnungs-JSON |
Rechnungen::ERechnungXML | Container | erzeugte strukturierte Rechnung |
Rechnungen::SichtPDF | Container | sichtbares Rechnungs-PDF |
Rechnungen::PDF_A_3 | Container | fertige hybride PDF/A-3-Datei |
Rechnungen::API_Antwort | Text | vollständige Antwort des letzten API-Aufrufs |
Rechnungen::Erstellungsstatus | Text | technischer und fachlicher Gesamtstatus |
Rechnungen::Validierungsbericht | Text/Container | vollständiger Validierungsbericht |
In der Beispielimplementierung werden unter anderem diese Modelle verwendet:
| Modell | Verwendung |
|---|---|
Facturxen16931 | Factur-X/ZUGFeRD auf Basis von EN 16931 |
Ubl21 | UBL 2.1 Invoice |
Ubl21cn | UBL 2.1 Credit Note |
Cii16buncoupled | UN/CEFACT CII D16B, uncoupled |
Cii16bcoupled | UN/CEFACT CII D16B, coupled |
Wichtiger Unterschied zwischen EN16931 und EXTENDED:
ZUGFeRD/Factur-X gibt es in der aktuellen Version 2.5.2 auch im Profil EXTENDED. Dieses Profil basiert auf EN 16931, geht aber über deren semantisches Datenmodell hinaus und ermöglicht zusätzliche Strukturen und Kardinalitäten. Während bestimmte Elemente im EN16931-Profil nur einmal vorkommen können, sind sie im EXTENDED-Profil als wiederholbare Elemente abgebildet.
Dazu gehören unter anderem:
applicable_trade_taxdefined_trade_contactspecified_trade_payment_termsDas ist ein wichtiger Unterschied für die technische Umsetzung: EXTENDED ist nicht einfach EN16931 mit einigen zusätzlichen Feldern.
Der Webdienst kann Rechnungen in diesem Profil über den separaten Endpunkt
POST <API-BASIS-URL>/Facturxextended erzeugen.
Der Schema-Endpunkt liefert die technisch verfügbaren XML- und JSON-Pfade sowie die erwarteten Datentypen. Er ersetzt nicht die fachliche Dokumentation des gewählten Rechnungsprofils. Das Vorhandensein eines Elements im Schema bedeutet nicht, dass es im jeweiligen Geschäftsfall zulässig, erforderlich oder ausreichend ist.
GET <API-BASIS-URL>/<SCHEMA-ENDPUNKT>/<MODELLNAME>
Accept: application/json
x-api-key: <API-KEY>
Set Error Capture [ On ]
Allow User Abort [ Off ]
Set Variable [ $schemaName ; Value: "Facturxen16931" ]
Set Variable [ $apiKey ; Value: Einstellungen::API_Key ]
Set Variable [ $url ; Value:
"<API-BASIS-URL>/<SCHEMA-ENDPUNKT>/" & $schemaName
]
Set Variable [ $schemaResponse ; Value: "" ]
Set Variable [ $responseHeaders ; Value: "" ]
Set Variable [ $curlOptions ; Value:
"--request GET" & ¶ &
"--header " & Quote ( "Accept: application/json" ) & ¶ &
If ( not IsEmpty ( $apiKey ) ;
"--header " & Quote ( "x-api-key: " & $apiKey ) & ¶ ; "" ) &
"--dump-header $responseHeaders" & ¶ &
"--connect-timeout 15" & ¶ &
"--show-error" & ¶ &
"--max-time 120"
]
Insert from URL [
Select ;
With dialog: Off ;
Target: $schemaResponse ;
$url ;
Verify SSL Certificates ;
cURL options: $curlOptions
]
Set Variable [ $lastError ; Value:
JSONSetElement ( "{}" ;
[ "code" ; Get ( LastError ) ; JSONNumber ] ;
[ "detail" ; Get ( LastErrorDetail ) ; JSONString ]
)
]
Set Variable [ $fmError ; Value: JSONGetElement ( $lastError ; "code" ) ]
Set Variable [ $fmErrorDetail ; Value: JSONGetElement ( $lastError ; "detail" ) ]
Set Variable [ $formattedResponse ; Value:
JSONFormatElements ( $schemaResponse )
]
If [ $fmError ≠ 0 or Left ( $formattedResponse ; 1 ) = "?" ]
Exit Script [ Text Result: $lastError ]
End If
If [ JSONGetElementType ( $schemaResponse ; "" ) ≠ JSONArray ]
Exit Script [ Text Result: "Die Schema-Antwort ist kein JSON-Array." ]
End If
Set Variable [ $schemaElements ; Value:
JSONListValues ( $schemaResponse ; "" )
]
| Wert | FileMaker-Konstante |
|---|---|
| Text, Codes, IDs und Datumsstrings | JSONString |
| Mengen, Preise, Prozente und Summen | JSONNumber |
| Wahr/Falsch | JSONBoolean |
| Objekt oder Array | JSONObject / JSONArray |
Formatierte Zahlenstrings wie "1.234,56" dürfen nicht als Rechnungsbetrag übertragen werden.
Set Variable [ $json ; Value: "{}" ]
Set Variable [ $json ; Value:
JSONSetElement ( $json ;
[ "exchanged_document_context.guideline_specified_document_context_parameter.id.value" ;
"urn:cen.eu:en16931:2017" ; JSONString ] ;
[ "exchanged_document.id.value" ;
Rechnungskopfdaten::Rechnungsnummer ; JSONString ] ;
[ "exchanged_document.type_code.value" ;
Rechnungskopfdaten::Dokumententyp ; JSONString ] ;
[ "exchanged_document.issue_date_time.date_time_string.format" ;
"102" ; JSONString ] ;
[ "exchanged_document.issue_date_time.date_time_string.value" ;
dateformat102 ( Rechnungskopfdaten::Rechnungsdatum ) ; JSONString ] ;
[ "supply_chain_trade_transaction.applicable_header_trade_settlement.invoice_currency_code.value" ;
"EUR" ; JSONString ]
)
]
Das Beispiel zeigt zentrale Pfade. Pflichtfelder hängen von Dokumenttyp, Profil und Geschäftsfall ab. Käufer, Verkäufer, Steuern, Summen und Zahlungsdaten müssen entsprechend dem abgerufenen Schema ergänzt werden.
Go to Record/Request/Page [ First ]
Loop [ Flush: Always ]
Set Variable [ $i ; Value: Get ( RecordNumber ) - 1 ]
Set Variable [ $path ; Value:
"supply_chain_trade_transaction.included_supply_chain_trade_line_item[" & $i & "]"
]
Set Variable [ $json ; Value:
JSONSetElement ( $json ;
[ $path & ".associated_document_line_document.line_id.value" ;
Get ( RecordNumber ) ; JSONString ] ;
[ $path & ".specified_trade_product.name.value" ;
Rechnungspositionen::Artikelname ; JSONString ] ;
[ $path & ".specified_line_trade_delivery.billed_quantity.value" ;
Rechnungspositionen::Menge ; JSONNumber ] ;
[ $path & ".specified_line_trade_delivery.billed_quantity.unit_code" ;
Rechnungspositionen::EinheitenCode ; JSONString ] ;
[ $path & ".specified_line_trade_settlement.applicable_trade_tax.rate_applicable_percent.value" ;
Steuerschlüssel::Steuersatz ; JSONNumber ] ;
[ $path & ".specified_line_trade_settlement.specified_trade_settlement_line_monetary_summation.line_total_amount.value" ;
Rechnungspositionen::GesamtNetto ; JSONNumber ]
)
]
Go to Record/Request/Page [ Next ; Exit after last: On ]
End Loop
Set Variable [ $formattedJSON ; Value: JSONFormatElements ( $json ) ]
If [ Left ( $formattedJSON ; 1 ) = "?" ]
Exit Script [ Text Result: $formattedJSON ]
End If
If [ JSONGetElementType ( $json ; "" ) ≠ JSONObject ]
Exit Script [ Text Result: "Das Rechnungs-JSON muss ein Objekt sein." ]
End If
POST <API-BASIS-URL>/<JSON-ZU-XML-ENDPUNKT>/<MODELLNAME>
Content-Type: application/json
Body: <RECHNUNGS-JSON>
Response:
{ "message": "...", "xml_output": "<XML-KLARTEXT>" }
Set Variable [ $model ; Value: "Facturxen16931" ]
Set Variable [ $apiKey ; Value: Einstellungen::API_Key ]
Set Variable [ $url ; Value:
"<API-BASIS-URL>/<JSON-ZU-XML-ENDPUNKT>/" & $model
]
Set Variable [ $response ; Value: "" ]
Set Variable [ $responseHeaders ; Value: "" ]
Set Variable [ $requestData ; Value: $json ]
Set Variable [ $curlOptions ; Value:
"--request POST" & ¶ &
"--header " & Quote ( "Content-Type: application/json" ) & ¶ &
If ( not IsEmpty ( $apiKey ) ;
"--header " & Quote ( "x-api-key: " & $apiKey ) & ¶ ; "" ) &
"--data @$requestData" & ¶ &
"--dump-header $responseHeaders" & ¶ &
"--connect-timeout 15" & ¶ &
"--show-error" & ¶ &
"--max-time 120"
]
Insert from URL [
Select ;
With dialog: Off ;
Target: $response ;
$url ;
Verify SSL Certificates ;
cURL options: $curlOptions
]
Set Variable [ $lastError ; Value:
JSONSetElement ( "{}" ;
[ "code" ; Get ( LastError ) ; JSONNumber ] ;
[ "detail" ; Get ( LastErrorDetail ) ; JSONString ]
)
]
Set Variable [ $fmError ; Value: JSONGetElement ( $lastError ; "code" ) ]
Set Variable [ $fmErrorDetail ; Value: JSONGetElement ( $lastError ; "detail" ) ]
Set Field [ Rechnungen::API_Antwort ; $response ]
Die Fehlerwerte müssen innerhalb desselben ersten Set Variable-Schritts nach
Insert from URL erfasst werden. --data @$requestData übergibt den
Inhalt der FileMaker-Variablen direkt.
Set Variable [ $headerLines ; Value:
Substitute ( $responseHeaders ; Char ( 13 ) ; "" )
]
Set Variable [ $headerIndex ; Value: ValueCount ( $headerLines ) ]
Set Variable [ $httpStatusLine ; Value: "" ]
Loop [ Flush: Always ]
Exit Loop If [ $headerIndex < 1 ]
Set Variable [ $httpStatusLine ; Value:
GetValue ( $headerLines ; $headerIndex )
]
Exit Loop If [ Left ( $httpStatusLine ; 5 ) = "HTTP/" ]
Set Variable [ $headerIndex ; Value: $headerIndex - 1 ]
End Loop
Set Variable [ $httpStatus ; Value:
If ( Left ( $httpStatusLine ; 5 ) = "HTTP/" ;
GetAsNumber ( MiddleWords ( $httpStatusLine ; 2 ; 1 ) ) ; 0 )
]
Set Variable [ $formattedResponse ; Value: JSONFormatElements ( $response ) ]
If [ ( $fmError ≠ 0 and $httpStatus = 0 ) or
Left ( $formattedResponse ; 1 ) = "?" ]
Exit Script [ Text Result: $lastError ]
End If
If [ JSONGetElementType ( $response ; "" ) ≠ JSONObject ]
Exit Script [ Text Result: "Die API-Antwort ist kein JSON-Objekt." ]
End If
Set Variable [ $responseKeys ; Value: JSONListKeys ( $response ; "" ) ]
Set Variable [ $xmlText ; Value:
If ( not IsEmpty ( FilterValues ( $responseKeys ; "xml_output" ) ) ;
JSONGetElement ( $response ; "xml_output" ) ; "" )
]
Set Variable [ $apiDetail ; Value:
If ( not IsEmpty ( FilterValues ( $responseKeys ; "detail" ) ) ;
JSONGetElement ( $response ; "detail" ) ; "" )
]
If [ $httpStatus < 200 or $httpStatus ≥ 300 or
not IsEmpty ( $apiDetail ) or IsEmpty ( $xmlText ) ]
Exit Script [ Text Result:
JSONSetElement ( "{}" ;
[ "status" ; "api_error" ; JSONString ] ;
[ "http_status" ; $httpStatus ; JSONNumber ] ;
[ "detail" ; $apiDetail ; JSONString ]
)
]
End If
Set Variable [ $xmlContainer ; Value:
TextEncode ( $xmlText ; "utf-8" ; 3 )
]
Set Field [ Rechnungen::ERechnungXML ; $xmlContainer ]
Set Variable [ $pdfContainer ; Value: "" ]
Save Records as PDF [
Restore ;
Save to: Target ;
Target: $pdfContainer ;
With dialog: Off ;
Current record
]
Set Variable [ $pdfError ; Value: Get ( LastError ) ]
If [ $pdfError ≠ 0 or IsEmpty ( $pdfContainer ) ]
Exit Script [ Text Result: "Das Sicht-PDF konnte nicht erzeugt werden." ]
End If
Set Field [ Rechnungen::SichtPDF ; $pdfContainer ]
Das Rechnungs-Drucklayout und der richtige Datensatz müssen aktiv sein. Der Scriptschritt setzt das Zugriffsrecht Allow printing voraus.
If [ IsEmpty ( Rechnungen::SichtPDF ) or
IsEmpty ( Rechnungen::ERechnungXML ) ]
Exit Script [ Text Result: "Sicht-PDF oder Rechnungs-XML fehlt." ]
End If
Set Variable [ $requestData ; Value:
JSONSetElement ( "{}" ;
[ "pdf" ; Base64EncodeRFC ( 4648 ; Rechnungen::SichtPDF ) ; JSONString ] ;
[ "xml" ; Base64EncodeRFC ( 4648 ; Rechnungen::ERechnungXML ) ; JSONString ]
)
]
Set Variable [ $url ; Value: "<API-BASIS-URL>/<PDFA3-ENDPUNKT>" ]
Set Variable [ $apiKey ; Value: Einstellungen::API_Key ]
Set Variable [ $response ; Value: "" ]
Set Variable [ $responseHeaders ; Value: "" ]
Set Variable [ $curlOptions ; Value:
"--request POST" & ¶ &
"--header " & Quote ( "Content-Type: application/json" ) & ¶ &
If ( not IsEmpty ( $apiKey ) ;
"--header " & Quote ( "x-api-key: " & $apiKey ) & ¶ ; "" ) &
"--data @$requestData" & ¶ &
"--dump-header $responseHeaders" & ¶ &
"--connect-timeout 15" & ¶ &
"--show-error" & ¶ &
"--max-time 120"
]
Insert from URL [
Select ;
With dialog: Off ;
Target: $response ;
$url ;
Verify SSL Certificates ;
cURL options: $curlOptions
]
Set Variable [ $lastError ; Value:
JSONSetElement ( "{}" ;
[ "code" ; Get ( LastError ) ; JSONNumber ] ;
[ "detail" ; Get ( LastErrorDetail ) ; JSONString ]
)
]
Set Variable [ $fmError ; Value: JSONGetElement ( $lastError ; "code" ) ]
Set Variable [ $fmErrorDetail ; Value: JSONGetElement ( $lastError ; "detail" ) ]
Set Field [ Rechnungen::API_Antwort ; $response ]
# Determine $httpStatus with the block from step 5
Set Variable [ $formattedResponse ; Value: JSONFormatElements ( $response ) ]
If [ ( $fmError ≠ 0 and $httpStatus = 0 ) or
Left ( $formattedResponse ; 1 ) = "?" ]
Exit Script [ Text Result: $lastError ]
End If
If [ JSONGetElementType ( $response ; "" ) ≠ JSONObject ]
Exit Script [ Text Result: "Die API-Antwort ist kein JSON-Objekt." ]
End If
Set Variable [ $responseKeys ; Value: JSONListKeys ( $response ; "" ) ]
Set Variable [ $pdfBase64 ; Value:
If ( not IsEmpty ( FilterValues ( $responseKeys ; "pdf" ) ) ;
JSONGetElement ( $response ; "pdf" ) ; "" )
]
Set Variable [ $apiDetail ; Value:
If ( not IsEmpty ( FilterValues ( $responseKeys ; "detail" ) ) ;
JSONGetElement ( $response ; "detail" ) ; "" )
]
If [ $httpStatus < 200 or $httpStatus ≥ 300 or
not IsEmpty ( $apiDetail ) or IsEmpty ( $pdfBase64 ) ]
Exit Script [ Text Result:
JSONSetElement ( "{}" ;
[ "status" ; "api_error" ; JSONString ] ;
[ "http_status" ; $httpStatus ; JSONNumber ] ;
[ "detail" ; $apiDetail ; JSONString ] ;
[ "response" ; $response ; JSONString ]
)
]
End If
Set Variable [ $fileName ; Value:
"E-Rechnung " & Rechnungskopfdaten::Rechnungsnummer & ".pdf"
]
Set Variable [ $pdfaContainer ; Value:
Base64Decode ( $pdfBase64 ; $fileName )
]
Set Field [ Rechnungen::PDF_A_3 ; $pdfaContainer ]
Der Dateiname im zweiten Parameter von Base64Decode sorgt dafür, dass FileMaker
echte PDF-Containerdaten erzeugt. Übergeben Sie Base64 ohne ein
data:application/pdf;base64,-Präfix.
Die Endpunkte liefern unterschiedliche JSON-Strukturen. Speichern Sie die vollständige Antwort
immer in Rechnungen::API_Antwort und lesen Sie erwartete Felder erst aus, nachdem
HTTP-Status, JSON-Syntax und JSON-Wurzeltyp geprüft wurden.
[
[
"<XML-PFAD>",
"<JSON-PFAD>",
"<FELDTYP>",
"<XML-ELEMENT>"
],
[
"<XML-PFAD>",
"<JSON-PFAD>",
"<FELDTYP>",
"<XML-ELEMENT>"
]
]
| Position | Inhalt | Beispielziel in FileMaker |
|---|---|---|
| 1 | vollständiger XML-Pfad | Schema::Xml_Pfad |
| 2 | beim Request zu verwendender JSON-Pfad | Schema::Json_Pfad |
| 3 | erwarteter Datentyp | Schema::Feldtyp |
| 4 | zugehöriges XML-Element | Schema::Xml_Element |
Die Schema-Antwort ist auf Root-Ebene ein JSONArray. Jeder Array-Eintrag ist wiederum
ein Array mit vier Positionen. Ein Fehlerobjekt darf deshalb nicht wie eine Schemaliste importiert
werden.
{
"message": "Invoice processed successfully.",
"xml_output": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rsm:CrossIndustryInvoice>...</rsm:CrossIndustryInvoice>"
}
| Element | Datentyp | Bedeutung |
|---|---|---|
message | Text | allgemeine Erfolgsmeldung des Dienstes |
xml_output | Text | vollständiges Rechnungs-XML als Klartext, nicht Base64 |
Die Zeichen \" und \n gehören nur zur JSON-Darstellung.
JSONGetElement ( $response ; "xml_output" ) liefert den normalen XML-Text. Wandeln
Sie ihn anschließend mit TextEncode ( $xmlText ; "utf-8" ; 3 ) in Containerdaten um.
{
"message": "PDF/A wurde erfolgreich erstellt",
"pdf": "JVBERi0xLj..."
}
| Element | Datentyp | Bedeutung |
|---|---|---|
message | Text | allgemeine Erfolgsmeldung des Dienstes |
pdf | Text | vollständige PDF/A-3-Datei als Base64-kodierter Text |
pdf ist weder eine URL noch rohe Binärdaten. Dekodieren Sie den Wert mit
Base64Decode ( $pdfBase64 ; $fileName ); der Dateiname mit Endung .pdf
sorgt für echte PDF-Containerdaten.
{
"detail": "<FEHLERBESCHREIBUNG>"
}
Typische Beispiele sind:
{ "detail": "Invalid API Key" }
{ "detail": "Invalid JSON element" }
{ "detail": "XML is not valid." }
| Ebene | Prüfung | Bedeutung |
|---|---|---|
| FileMaker/cURL | Get ( LastError ) | Scriptschritt oder Übertragung fehlgeschlagen |
| Externe Bibliothek | Get ( LastErrorDetail ) | cURL-, TLS-, HTTP- oder Netzwerkdiagnose |
| HTTP | $httpStatus | Status außerhalb von 200 bis 299 |
| API | detail | fachlicher oder serverseitiger Fehler |
xml_output, pdf oder
detail auslesen. Eine HTTP-Antwort mit Status 200 ersetzt nicht die abschließende
fachliche Validierung.
| Status | Bedeutung |
|---|---|
transport_error | Der Dienst konnte nicht erreicht werden. |
invalid_response | Die Antwort war kein erwartetes JSON-Objekt. |
http_error | HTTP-Status außerhalb von 200 bis 299. |
xml_created | XML erzeugt, aber noch nicht abschließend validiert. |
pdfa3_created | PDF/A-3 erzeugt, aber noch nicht abschließend validiert. |
validation_failed | Schema-, Profil- oder Geschäftsregel verletzt. |
created_and_validated | Erzeugung und Validierung waren erfolgreich. |
Nur created_and_validated darf einen automatischen Versand freigeben.