This guide adds ZUGFeRD and XRechnung to SAP ECC and S/4HANA on-premise. You need three ABAP classes and one REST API. The code uses only cl_http_client and /ui2/cl_json, which are part of the standard system. No add-on, no SAP Document and Reporting Compliance, no BTP. The email section shows how the API sends the invoice to the customer. The import section shows how to check and read supplier invoices into SAP. If you run SAP BTP Integration Suite, read the SAP BTP guide instead. It shows the same integration as an iFlow.
Why SAP on-premise needs ZUGFeRD now
German companies on SAP ECC or S/4HANA on-premise must send and receive structured e-invoices. Since January 2025, every German business must accept e-invoices. From January 2027, businesses above EUR 800,000 turnover must send them. From 2028, all businesses must. The PDF from SAPscript, Smart Forms or Adobe Forms does not count. The law wants a hybrid file: a PDF the customer can open, with XML inside that their software can read. This file is ZUGFeRD. The Germany mandate page lists the dates and formats.
The SAP standard route is heavy for this. SAP Document and Reporting Compliance covers many countries and many processes. It is the right choice if you need clearance platforms in Italy, Poland or France. For a German company that needs ZUGFeRD and XRechnung, it is a project. An ABAP class with one HTTP call is an afternoon.
The integration does not change when the formats change. ZUGFeRD, XRechnung and the EN 16931 rule sets get new versions. The API applies them on their effective dates. Your ABAP sends the same JSON before and after. Nothing to transport, nothing to retest.
What you need in SAP
- Any release since NetWeaver 7.02 (ECC 6.0 EHP5 or later, and every S/4HANA release). The code in this guide uses no syntax newer than 7.02. See older releases for systems without
/ui2/cl_json. - HTTPS from the application server. Import the certificate chain of
api.invoicexml.cominto STRUST, SSL client (Anonymous). The kernel must support TLS 1.2. - One customizing table for the API key and the sender address.
- ArchiveLink for storing the PDF on the billing document (OAC0 and OAC3). A GOS attachment works too.
- An InvoiceXML account. Create one. You get an API key and 100 free credits. No credit card is needed.
How the integration works
A billing document is saved in SAP. Your class reads it, builds a JSON body and posts it to one endpoint. The API calculates the totals and the VAT, creates the XML, embeds it in a PDF/A-3 file, validates it and returns the PDF. Your class stores the PDF with ArchiveLink, or lets the API email it to the customer. One call, one validated file.
| Customer | Format | Endpoint | Result |
|---|---|---|---|
| German business (B2B) | ZUGFeRD EN 16931 | /v1/create/zugferd | Hybrid PDF/A-3 |
| German business, delivered by the API | ZUGFeRD EN 16931 | /v1/create/zugferd/email | Email with the PDF attached |
| German public buyer (B2G) | XRechnung | /v1/create/xrechnung | XML with Leitweg-ID |
| French business | Factur-X EN 16931 | /v1/create/facturx | Hybrid PDF/A-3 |
All endpoints take the same JSON body. You create four new objects in your own namespace. They are not standard SAP objects. You create them from the listings in this guide.
- Table ZINVXML_SETUP: the API key, the base URL and the sender address.
- Class ZCL_INVXML_CLIENT: the HTTP calls (JSON and file upload).
- Class ZCL_INVXML_CREATE: maps the billing document to JSON, creates the file, archives it, sends it by email.
- Class ZCL_INVXML_IMPORT: validates incoming invoices, sends the report back, reads the data and creates the SAP document.
- Exception class ZCX_INVXML with one attribute
text. Every error from the API ends in this exception.
Everything else is standard SAP: VBRK and VBRP, T001, KNA1, ADRC, LFA1, ArchiveLink, SAPconnect and the invoice BAPIs.
Step 1: Store the API key and call the API
Store the key in a customizing table. Give the table its own authorization group so only the integration role can read it. If your release has the secure storage, store the key there instead and read it in the open method. The client class wraps cl_http_client. It has one method for JSON bodies and one for file uploads. Every other class in this guide calls these two methods.
" Table ZINVXML_SETUP (delivery class C, one row per client)
" MANDT CLNT 3
" API_KEY CHAR 250
" BASE_URL CHAR 250 default https://api.invoicexml.com
" FROM_EMAIL CHAR 241 sender address for the email endpoint
CLASS zcl_invxml_client DEFINITION PUBLIC CREATE PUBLIC.
PUBLIC SECTION.
TYPES: BEGIN OF ty_result,
status TYPE i, " HTTP status code
valid TYPE abap_bool, " X-Invoice-Valid header of the report endpoints
data TYPE xstring, " the returned file
text TYPE string, " the returned JSON or error text
END OF ty_result.
METHODS post_json
IMPORTING iv_path TYPE string
iv_body TYPE string
RETURNING VALUE(rs_result) TYPE ty_result.
METHODS post_file
IMPORTING iv_path TYPE string
iv_file TYPE xstring
iv_filename TYPE string
iv_language TYPE string OPTIONAL
RETURNING VALUE(rs_result) TYPE ty_result.
PRIVATE SECTION.
METHODS open
IMPORTING iv_path TYPE string
RETURNING VALUE(ro_client) TYPE REF TO if_http_client.
METHODS send
IMPORTING io_client TYPE REF TO if_http_client
RETURNING VALUE(rs_result) TYPE ty_result.
ENDCLASS.
CLASS zcl_invxml_client IMPLEMENTATION.
METHOD open.
DATA ls_setup TYPE zinvxml_setup.
SELECT SINGLE * FROM zinvxml_setup INTO ls_setup.
IF ls_setup-api_key IS INITIAL.
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = 'No API key in ZINVXML_SETUP'.
ENDIF.
cl_http_client=>create_by_url(
EXPORTING url = ls_setup-base_url && iv_path
ssl_id = 'ANONYM'
IMPORTING client = ro_client ).
ro_client->request->set_method( if_http_request=>co_request_method_post ).
ro_client->request->set_header_field(
name = 'Authorization'
value = |Bearer { ls_setup-api_key }| ).
ENDMETHOD.
METHOD post_json.
DATA lo_client TYPE REF TO if_http_client.
lo_client = open( iv_path ).
lo_client->request->set_content_type( 'application/json' ).
lo_client->request->set_cdata( iv_body ).
rs_result = send( lo_client ).
ENDMETHOD.
METHOD post_file.
DATA: lo_client TYPE REF TO if_http_client,
lo_part TYPE REF TO if_http_entity.
lo_client = open( iv_path ).
" The upload endpoints take a multipart form with one field named "file"
lo_part = lo_client->request->add_multipart( ).
lo_part->set_header_field(
name = 'Content-Disposition'
value = |form-data; name="file"; filename="{ iv_filename }"| ).
lo_part->set_content_type( 'application/octet-stream' ).
lo_part->set_data( iv_file ).
IF iv_language IS NOT INITIAL.
" Optional: language of the validation report (en, de, fr)
lo_part = lo_client->request->add_multipart( ).
lo_part->set_header_field(
name = 'Content-Disposition'
value = 'form-data; name="validationLanguage"' ).
lo_part->set_cdata( iv_language ).
ENDIF.
rs_result = send( lo_client ).
ENDMETHOD.
METHOD send.
io_client->send( ).
io_client->receive( ).
io_client->response->get_status( IMPORTING code = rs_result-status ).
rs_result-data = io_client->response->get_data( ).
rs_result-text = io_client->response->get_cdata( ).
IF io_client->response->get_header_field( 'X-Invoice-Valid' ) = 'true'.
rs_result-valid = abap_true.
ENDIF.
io_client->close( ).
ENDMETHOD.
ENDCLASS.
add_multipart builds the multipart form body and sets the boundary. The validation report endpoints answer with a PDF and put the verdict in the X-Invoice-Valid header, so the send method reads that header into the result. The other endpoints ignore it.
Step 2: Map SAP fields to JSON
The API takes an EN 16931 invoice as JSON. All the data is on the billing document, the company code and the customer master. This is the mapping:
| SAP field | JSON path | Note |
|---|---|---|
VBRK-VBELN | invoice.invoiceNumber | BT-1 |
VBRK-FKDAT | invoice.issueDate | Format as yyyy-MM-dd |
VBRK-WAERK | invoice.currency | ISO code, as stored |
T001-BUTXT, T001-STCEG, address from ADRC | invoice.seller | The seller VAT id is required |
KNA1-NAME1, KNA1-STCEG, address from ADRC | invoice.buyer | Payer (VBRK-KUNRG) or sold-to (VBRK-KUNAG), your choice |
House bank IBAN (T012K, TIBAN) | invoice.paymentDetails.paymentAccountIdentifier | Recommended for B2B |
VBRP-ARKTX | lines[].item.name | Per line |
VBRP-FKIMG / VBRP-NETWR | lines[].quantity / lines[].priceDetails.netPrice | Net price per unit, after discounts. The API calculates the totals |
VBRP-VRKME | lines[].unitCode | ISO code via UNIT_OF_MEASURE_SAP_TO_ISO |
| MWST condition of the line | lines[].vatInformation.rate | KONV on ECC, PRCD_ELEMENTS on S/4HANA |
| Leitweg-ID (custom field) | invoice.buyerReference | XRechnung only |
You do not send totals, rounding or VAT amounts. The API calculates them from the lines. If the numbers do not add up, the API returns an error instead of a file. This catches rounding and condition problems before the customer sees them.
In ABAP, the body is a nested structure. /ui2/cl_json turns it into JSON and converts the component names to camelCase. The same structure reads the JSON back when you import supplier invoices, so you define it once.
CLASS zcl_invxml_create DEFINITION PUBLIC CREATE PUBLIC.
PUBLIC SECTION.
" The component names become the JSON field names in camelCase:
" VAT_IDENTIFIER -> vatIdentifier, PRICE_DETAILS -> priceDetails
TYPES: BEGIN OF ty_address,
line1 TYPE string,
city TYPE string,
post_code TYPE string,
country TYPE string,
END OF ty_address,
BEGIN OF ty_contact,
name TYPE string,
phone TYPE string,
email TYPE string,
END OF ty_contact,
BEGIN OF ty_eaddress,
identifier TYPE string,
scheme_id TYPE string,
END OF ty_eaddress,
BEGIN OF ty_party,
name TYPE string,
vat_identifier TYPE string,
postal_address TYPE ty_address,
contact TYPE ty_contact, " XRechnung only
electronic_address TYPE ty_eaddress, " XRechnung only
END OF ty_party,
BEGIN OF ty_payment,
payment_account_identifier TYPE string,
END OF ty_payment,
BEGIN OF ty_item,
name TYPE string,
END OF ty_item,
BEGIN OF ty_price,
net_price TYPE p LENGTH 13 DECIMALS 2,
END OF ty_price,
BEGIN OF ty_vat,
rate TYPE p LENGTH 5 DECIMALS 2,
END OF ty_vat,
BEGIN OF ty_line,
quantity TYPE p LENGTH 13 DECIMALS 3,
unit_code TYPE string,
item TYPE ty_item,
price_details TYPE ty_price,
vat_information TYPE ty_vat,
END OF ty_line,
BEGIN OF ty_totals,
grand_total_amount TYPE p LENGTH 13 DECIMALS 2,
due_payable_amount TYPE p LENGTH 13 DECIMALS 2,
END OF ty_totals,
BEGIN OF ty_invoice,
invoice_number TYPE string,
issue_date TYPE string,
due_date TYPE string,
currency TYPE string,
buyer_reference TYPE string,
purchase_order_reference TYPE string,
seller TYPE ty_party,
buyer TYPE ty_party,
payment_details TYPE ty_payment,
lines TYPE STANDARD TABLE OF ty_line WITH DEFAULT KEY,
totals TYPE ty_totals, " filled by the API on extraction
END OF ty_invoice,
BEGIN OF ty_email,
from TYPE string,
to TYPE string,
subject TYPE string,
message TYPE string,
END OF ty_email,
BEGIN OF ty_body,
invoice TYPE ty_invoice,
email TYPE ty_email, " only for the email endpoints
END OF ty_body.
METHODS build_body
IMPORTING iv_vbeln TYPE vbeln_vf
RETURNING VALUE(rs_body) TYPE ty_body.
METHODS create_hybrid
IMPORTING iv_vbeln TYPE vbeln_vf
RETURNING VALUE(rv_pdf) TYPE xstring.
METHODS archive_pdf
IMPORTING iv_vbeln TYPE vbeln_vf
iv_pdf TYPE xstring.
METHODS send_by_email
IMPORTING iv_vbeln TYPE vbeln_vf
iv_to TYPE string.
PRIVATE SECTION.
METHODS address_from
IMPORTING is_adrc TYPE adrc
RETURNING VALUE(rs_address) TYPE ty_address.
METHODS tax_rate
IMPORTING is_vbrk TYPE vbrk
is_vbrp TYPE vbrp
RETURNING VALUE(rv_rate) TYPE ty_vat-rate.
METHODS iso_unit
IMPORTING iv_unit TYPE meins
RETURNING VALUE(rv_unit) TYPE string.
METHODS house_bank_iban
IMPORTING iv_bukrs TYPE bukrs
RETURNING VALUE(rv_iban) TYPE string.
ENDCLASS.
The mapping method reads the billing document and fills the structure:
METHOD build_body.
DATA: ls_vbrk TYPE vbrk,
lt_vbrp TYPE STANDARD TABLE OF vbrp,
ls_vbrp TYPE vbrp,
ls_t001 TYPE t001,
ls_kna1 TYPE kna1,
ls_adrc TYPE adrc,
ls_line TYPE ty_line.
SELECT SINGLE * FROM vbrk INTO ls_vbrk WHERE vbeln = iv_vbeln.
SELECT * FROM vbrp INTO TABLE lt_vbrp WHERE vbeln = iv_vbeln.
SELECT SINGLE * FROM t001 INTO ls_t001 WHERE bukrs = ls_vbrk-bukrs.
SELECT SINGLE * FROM kna1 INTO ls_kna1 WHERE kunnr = ls_vbrk-kunrg.
rs_body-invoice-invoice_number = ls_vbrk-vbeln.
rs_body-invoice-issue_date = |{ ls_vbrk-fkdat(4) }-{ ls_vbrk-fkdat+4(2) }-{ ls_vbrk-fkdat+6(2) }|.
rs_body-invoice-currency = ls_vbrk-waerk.
" Seller: the company code and its address
SELECT SINGLE * FROM adrc INTO ls_adrc WHERE addrnumber = ls_t001-adrnr.
rs_body-invoice-seller-name = ls_t001-butxt.
rs_body-invoice-seller-vat_identifier = ls_t001-stceg.
rs_body-invoice-seller-postal_address = address_from( ls_adrc ).
" Buyer: the payer and its address
SELECT SINGLE * FROM adrc INTO ls_adrc WHERE addrnumber = ls_kna1-adrnr.
rs_body-invoice-buyer-name = ls_kna1-name1.
rs_body-invoice-buyer-vat_identifier = ls_kna1-stceg.
rs_body-invoice-buyer-postal_address = address_from( ls_adrc ).
rs_body-invoice-payment_details-payment_account_identifier = house_bank_iban( ls_vbrk-bukrs ).
" One line per billing item. Text items have no quantity and are skipped.
LOOP AT lt_vbrp INTO ls_vbrp WHERE fkimg <> 0.
CLEAR ls_line.
ls_line-quantity = ls_vbrp-fkimg.
ls_line-unit_code = iso_unit( ls_vbrp-vrkme ).
ls_line-item-name = ls_vbrp-arktx.
ls_line-price_details-net_price = ls_vbrp-netwr / ls_vbrp-fkimg.
ls_line-vat_information-rate = tax_rate( is_vbrk = ls_vbrk is_vbrp = ls_vbrp ).
APPEND ls_line TO rs_body-invoice-lines.
ENDLOOP.
ENDMETHOD.
Four small private methods do the lookups. address_from maps ADRC (street and house number, post code, city, country). iso_unit calls UNIT_OF_MEASURE_SAP_TO_ISO. tax_rate reads the MWST condition for the line through VBRK-KNUMV and VBRP-POSNR. house_bank_iban reads the IBAN of the house bank from T012K and TIBAN. compress = abap_true drops empty fields, so the XRechnung and email fields only appear when you fill them.
Step 3: Create the ZUGFeRD invoice
This method sends the JSON and receives the PDF. The caller can archive it, send it or show it.
METHOD create_hybrid.
DATA: lo_client TYPE REF TO zcl_invxml_client,
ls_result TYPE zcl_invxml_client=>ty_result,
lv_json TYPE string.
lv_json = /ui2/cl_json=>serialize(
data = build_body( iv_vbeln )
compress = abap_true
pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
CREATE OBJECT lo_client.
ls_result = lo_client->post_json(
iv_path = '/v1/create/zugferd'
iv_body = lv_json ).
IF ls_result-status <> 200.
" 400 returns JSON with an errors array: one entry per violated rule,
" with the rule id and the JSON path of the field
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = ls_result-text.
ENDIF.
rv_pdf = ls_result-data.
ENDMETHOD.
For French customers, post the same body to /v1/create/facturx. ZUGFeRD and Factur-X are the same standard under two names. The Factur-X API toolkit covers the French side.
Step 4: Archive the PDF
Store the PDF on the billing document with ArchiveLink. It then shows in the attachment list of VF03, and your audit trail keeps the file that the customer received. Set up the content repository in OAC0 and the link (object type VBRK, document type ZZUGFERD) in OAC3.
METHOD archive_pdf.
DATA: lt_bin TYPE STANDARD TABLE OF tbl1024,
lv_length TYPE i,
lv_doc_id TYPE saeardoid.
CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
EXPORTING buffer = iv_pdf
IMPORTING output_length = lv_length
TABLES binary_tab = lt_bin.
" Content repository and document type are set up in OAC0 and OAC3
" (object type VBRK, document type ZZUGFERD)
CALL FUNCTION 'ARCHIVOBJECT_CREATE_TABLE'
EXPORTING archiv_id = 'A1'
document_type = 'PDF'
length = lv_length
IMPORTING archiv_doc_id = lv_doc_id
TABLES binarchivobject = lt_bin.
CALL FUNCTION 'ARCHIV_CONNECTION_INSERT'
EXPORTING archiv_id = 'A1'
arc_doc_id = lv_doc_id
ar_object = 'ZZUGFERD'
object_id = iv_vbeln
sap_object = 'VBRK'.
ENDMETHOD.
When to call it. For a manual button, add a function to the billing document display. For automatic creation, call create_hybrid and archive_pdf from the processing routine of a custom output type (NACE, application V3) on ECC, or from an output management action on S/4HANA. For a batch, run a report over VBRK by billing date.
Sending from SAP. If SAP sends the invoice, attach the archived PDF to the SAPconnect mail with cl_bcs instead of the Smart Forms or Adobe Forms PDF. The hybrid PDF already contains the readable invoice. The customer gets one PDF that opens normally and that their software can also read.
Send the invoice by email from the API
You can skip the sending in SAP. The API creates the invoice, validates it and emails it to the customer in one call. SAP never handles the file. This is enough for Germany: there is no central platform for B2B e-invoices, and an email with a ZUGFeRD PDF is a complete delivery.
Set up your sending domain once in the dashboard: add the domain and publish the three CNAME records it shows. From then on, every address on that domain can send. The customer sees your address, and replies reach you. The request is the same JSON body plus an email object:
METHOD send_by_email.
DATA: lo_client TYPE REF TO zcl_invxml_client,
ls_result TYPE zcl_invxml_client=>ty_result,
ls_setup TYPE zinvxml_setup,
ls_body TYPE ty_body,
lv_json TYPE string.
SELECT SINGLE * FROM zinvxml_setup INTO ls_setup.
ls_body = build_body( iv_vbeln ).
ls_body-email-from = ls_setup-from_email.
ls_body-email-to = iv_to.
ls_body-email-subject = |Rechnung { iv_vbeln }|.
ls_body-email-message = |Guten Tag,\n\nanbei erhalten Sie unsere Rechnung { iv_vbeln } | &&
|als ZUGFeRD-PDF.\n\nMit freundlichen Grüßen|.
lv_json = /ui2/cl_json=>serialize(
data = ls_body
compress = abap_true
pretty_name = /ui2/cl_json=>pretty_mode-camel_case ).
CREATE OBJECT lo_client.
ls_result = lo_client->post_json(
iv_path = '/v1/create/zugferd/email'
iv_body = lv_json ).
IF ls_result-status <> 200.
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = ls_result-text.
ENDIF.
" 200 means: created, validated and handed to delivery
ENDMETHOD.
Use this route when the API should send. Use the plain endpoint from step 3 when SAP should archive the file and send it itself. The email endpoint also exists for XRechnung (/v1/create/xrechnung/email) and Factur-X.
Import incoming supplier invoices
Since January 2025, German companies must accept e-invoices. More and more supplier invoices arrive as ZUGFeRD PDFs and XRechnung XML files. The pipeline has three steps: validate, send the report back if the invoice is invalid, extract the data if it is valid. SAPconnect inbound processing or a watched folder delivers the files to the class.
- Validate. Send the file to
POST /v1/validate/zugferd/reportor/v1/validate/xrechnung/report. The API checks the file against EN 16931 and the German rules. The response is a PDF validation report, and the headerX-Invoice-Validsays if the invoice passed. ThevalidationLanguagefield sets the language of the report, here German. If you only want the findings as JSON, use/v1/validate/zugferdwithout/report. - Reply with the report. If the invoice is invalid, email the PDF report to the sender with
cl_bcsand stop. The report lists every violated rule with the field and the message. The supplier can fix the invoice without a call to your accounting team. - Extract. If the invoice is valid, send the file to
POST /v1/extract/json. The API reads the embedded XML, or the XRechnung XML, and returns it as JSON in the same shape as the create request./ui2/cl_jsonreads it into thety_bodystructure from step 2.
CLASS zcl_invxml_import DEFINITION PUBLIC CREATE PUBLIC.
PUBLIC SECTION.
METHODS process_inbound
IMPORTING iv_file TYPE xstring
iv_filename TYPE string
iv_sender TYPE string
iv_bukrs TYPE bukrs.
METHODS check_file
IMPORTING iv_file TYPE xstring
iv_filename TYPE string
EXPORTING ev_valid TYPE abap_bool
ev_report TYPE xstring.
METHODS extract
IMPORTING iv_file TYPE xstring
iv_filename TYPE string
RETURNING VALUE(rs_invoice) TYPE zcl_invxml_create=>ty_invoice.
METHODS create_invoice
IMPORTING is_invoice TYPE zcl_invxml_create=>ty_invoice
iv_bukrs TYPE bukrs
RETURNING VALUE(rv_belnr) TYPE belnr_d.
PRIVATE SECTION.
METHODS reply_with_report
IMPORTING iv_sender TYPE string
iv_report TYPE xstring.
ENDCLASS.
CLASS zcl_invxml_import IMPLEMENTATION.
METHOD process_inbound.
DATA: lv_valid TYPE abap_bool,
lv_report TYPE xstring,
ls_invoice TYPE zcl_invxml_create=>ty_invoice.
" 1. Validate. The report comes back in the same call.
check_file( EXPORTING iv_file = iv_file
iv_filename = iv_filename
IMPORTING ev_valid = lv_valid
ev_report = lv_report ).
" 2. Invalid: send the PDF report to the sender and stop
IF lv_valid = abap_false.
reply_with_report( iv_sender = iv_sender iv_report = lv_report ).
RETURN.
ENDIF.
" 3. Valid: read the data and create the SAP document
ls_invoice = extract( iv_file = iv_file iv_filename = iv_filename ).
create_invoice( is_invoice = ls_invoice iv_bukrs = iv_bukrs ).
ENDMETHOD.
METHOD check_file.
DATA: lo_client TYPE REF TO zcl_invxml_client,
ls_result TYPE zcl_invxml_client=>ty_result,
lv_path TYPE string.
" PDF files are ZUGFeRD, XML files are XRechnung
lv_path = '/v1/validate/zugferd/report'.
IF iv_filename CP '*.xml'.
lv_path = '/v1/validate/xrechnung/report'.
ENDIF.
CREATE OBJECT lo_client.
ls_result = lo_client->post_file(
iv_path = lv_path
iv_file = iv_file
iv_filename = iv_filename
iv_language = 'de' ).
IF ls_result-status <> 200.
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = ls_result-text.
ENDIF.
ev_valid = ls_result-valid. " X-Invoice-Valid header
ev_report = ls_result-data. " the PDF report, valid or not
ENDMETHOD.
METHOD extract.
DATA: lo_client TYPE REF TO zcl_invxml_client,
ls_result TYPE zcl_invxml_client=>ty_result,
ls_body TYPE zcl_invxml_create=>ty_body.
CREATE OBJECT lo_client.
ls_result = lo_client->post_file(
iv_path = '/v1/extract/json'
iv_file = iv_file
iv_filename = iv_filename ).
IF ls_result-status <> 200.
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = ls_result-text.
ENDIF.
" The JSON has the same shape as the create request
/ui2/cl_json=>deserialize(
EXPORTING json = ls_result-text
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_body ).
rs_invoice = ls_body-invoice.
ENDMETHOD.
reply_with_report is a normal SAPconnect mail: cl_document_bcs=>create_document with a short text, add_attachment with type PDF and the report bytes, cl_bcs=>send to the sender address. Keep the invalid file in Incoming Documents or ArchiveLink too, so the case is on record.
Plain PDFs: some suppliers still send PDFs without XML. Send those to POST /v1/parse/json. The API reads typed, scanned and photographed invoices with AI and returns the same JSON plus a confidence object with scores between 0.0 and 1.0. Your code can post invoices with a high score and park the rest for review. Files that are not invoices are rejected with error code 4008.
Create the SAP invoice from the JSON
The JSON from both endpoints has the same shape, so one method creates the SAP document. It finds the vendor by the VAT id from the invoice, fills the header and adds one item per invoice line. For invoices with a purchase order, use BAPI_INCOMINGINVOICE_CREATE (MM logistics invoice verification). For invoices without a purchase order, use BAPI_ACC_DOCUMENT_POST with the vendor line and one G/L line per VAT rate. The example shows the purchase order case.
METHOD create_invoice.
DATA: ls_vendor TYPE lfa1,
ls_header TYPE bapi_incinv_create_header,
lt_items TYPE STANDARD TABLE OF bapi_incinv_create_item,
ls_item TYPE bapi_incinv_create_item,
lt_return TYPE STANDARD TABLE OF bapiret2,
ls_return TYPE bapiret2,
ls_line TYPE zcl_invxml_create=>ty_line,
lv_belnr TYPE bapi_incinv_fld-inv_doc_no,
lv_gjahr TYPE bapi_incinv_fld-fisc_year,
lv_index TYPE i.
" The vendor comes from the VAT id the API read from the invoice
SELECT SINGLE * FROM lfa1 INTO ls_vendor
WHERE stceg = is_invoice-seller-vat_identifier.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_invxml
EXPORTING text = |No vendor with VAT id { is_invoice-seller-vat_identifier }|.
ENDIF.
ls_header-invoice_ind = 'X'.
ls_header-doc_date = to_sap_date( is_invoice-issue_date ).
ls_header-pstng_date = sy-datum.
ls_header-ref_doc_no = is_invoice-invoice_number.
ls_header-comp_code = iv_bukrs.
ls_header-currency = is_invoice-currency.
ls_header-gross_amount = is_invoice-totals-grand_total_amount.
ls_header-calc_tax_ind = 'X'.
" One item per invoice line, against the purchase order from BT-13.
" Replace the position match with your own rule (material, PO text, delivery note).
LOOP AT is_invoice-lines INTO ls_line.
lv_index = lv_index + 1.
CLEAR ls_item.
ls_item-invoice_doc_item = lv_index.
ls_item-po_number = is_invoice-purchase_order_reference.
ls_item-po_item = lv_index * 10.
ls_item-quantity = ls_line-quantity.
ls_item-po_unit = sap_unit( ls_line-unit_code ).
ls_item-item_amount = ls_line-quantity * ls_line-price_details-net_price.
ls_item-tax_code = tax_code( ls_line-vat_information-rate ).
APPEND ls_item TO lt_items.
ENDLOOP.
CALL FUNCTION 'BAPI_INCOMINGINVOICE_CREATE'
EXPORTING headerdata = ls_header
IMPORTING invoicedocnumber = lv_belnr
fiscalyear = lv_gjahr
TABLES itemdata = lt_items
return = lt_return.
LOOP AT lt_return INTO ls_return WHERE type CA 'EA'.
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
RAISE EXCEPTION TYPE zcx_invxml EXPORTING text = ls_return-message.
ENDLOOP.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT' EXPORTING wait = 'X'.
rv_belnr = lv_belnr.
ENDMETHOD.
to_sap_date turns 2026-11-05 into 20261105. sap_unit is the reverse of iso_unit (UNIT_OF_MEASURE_ISO_TO_SAP). tax_code maps the VAT rate from the invoice to your tax code (19 to V1, 7 to V2, 0 to V0 in a standard German chart). If the rate on the invoice does not match the purchase order, the BAPI returns an error and the invoice waits for review.
XRechnung for German public buyers
Invoices to German federal, state and municipal buyers must be XRechnung, not ZUGFeRD. XRechnung is pure XML with no PDF. Portals such as ZRE and OZG-RE validate it against the KoSIT rules. The same class creates it. You need a third endpoint and a few more fields:
" In build_body, for public-sector customers:
rs_body-invoice-buyer_reference = leitweg_id( ls_vbrk-kunrg ). " a custom field on the customer master
rs_body-invoice-seller-contact-name = 'Buchhaltung'.
rs_body-invoice-seller-contact-phone = '+49 30 1234567'.
rs_body-invoice-seller-contact-email = '[email protected]'.
rs_body-invoice-seller-electronic_address-identifier = ls_t001-stceg.
rs_body-invoice-seller-electronic_address-scheme_id = '9930'. " 9930 = German VAT id
rs_body-invoice-buyer-electronic_address-identifier = ls_kna1-stceg.
rs_body-invoice-buyer-electronic_address-scheme_id = '9930'.
" Then post to /v1/create/xrechnung instead of /v1/create/zugferd.
" The response is XRechnung XML, not a PDF.
The Leitweg-ID is the routing id the public buyer gives you when you register as a supplier. Rule BR-DE-15 rejects invoices without it. Store it in a custom field on the customer master and map it into buyerReference. The seller contact (name, phone, email) and the electronic addresses are required by the German rules. Scheme 9930 means a German VAT id. For Peppol delivery, use the Peppol participant id and its scheme instead.
The API returns the XML, validated against the KoSIT rules. Deliver it the way the buyer requires: upload to ZRE or OZG-RE, Peppol, or email where the buyer accepts it. /v1/render/xrechnung/to/pdf creates a readable PDF preview for your own records. XRechnung is also valid for B2B invoices, so the /email route works for it too. The XRechnung API toolkit covers the format in detail.
Older releases: JSON and TLS
JSON. /ui2/cl_json ships with the UI add-on on NetWeaver 7.40 and later. On older systems, install SAP's standalone copy of the same class (ZCL_JSON, published by SAP on GitHub under the name abap-to-json). It has the same methods. If you do not want to install anything, build the JSON with string templates. The field mapping is the same as in the table above. Escape quotes and line breaks in the text fields.
TLS. api.invoicexml.com accepts TLS 1.2 and later. Older kernels need a current CommonCryptoLib and the profile parameters from SAP Note 510007 (ssl/client_ciphersuites). Test the connection with transaction SMICM or a small report that calls cl_http_client=>create_by_url and prints the status. A status 0 with error ICM_HTTP_SSL_ERROR means the certificate chain is missing in STRUST.
Syntax. The code in this guide uses string templates and &&, which need 7.02. It does not use inline declarations or COND, so it compiles on ECC 6.0 EHP5 and later.
ABAP or SAP BTP
Both routes call the same endpoints with the same JSON body. The difference is where the call runs. ABAP runs it inside SAP, next to the billing document, with no extra system. Use it when you have no BTP subscription or when the integration team lives in the ABAP stack. BTP Integration Suite runs it in an iFlow outside SAP. Use it when you already route documents through BTP or want the mapping in Cloud Integration. The SAP BTP guide shows that route step by step. You can start with ABAP and move the call later. The JSON does not change.
Endpoint reference
| Purpose | Endpoint | Input | Output |
|---|---|---|---|
| Create ZUGFeRD | POST /v1/create/zugferd | Invoice JSON | Hybrid PDF/A-3 |
| Create and email ZUGFeRD | POST /v1/create/zugferd/email | Invoice JSON + email object | Email sent from your domain |
| Create XRechnung | POST /v1/create/xrechnung | Invoice JSON with Leitweg-ID | XRechnung XML |
| Create and email XRechnung | POST /v1/create/xrechnung/email | Invoice JSON + email object | Email sent from your domain |
| Create Factur-X | POST /v1/create/facturx | Invoice JSON | Hybrid PDF/A-3 |
| Validate with PDF report | POST /v1/validate/zugferd/report, /xrechnung/report | PDF or XML file | PDF report, X-Invoice-Valid header |
| Validate as JSON | POST /v1/validate/zugferd, /xrechnung | PDF or XML file | Findings per rule |
| Extract structured data | POST /v1/extract/json | Hybrid PDF or XML file | Invoice JSON |
| Parse plain PDF | POST /v1/parse/json | Any PDF or image | Invoice JSON + confidence |
| Extract attachments | POST /v1/extract/attachments | Hybrid PDF or XML file | ZIP of embedded files |
| Render XRechnung | POST /v1/render/xrechnung/to/pdf | XRechnung XML | Readable PDF |
Get started
The whole integration is the three classes in this guide: call the API, build the JSON, create and archive or email the file, check and import what comes in. No add-on in SAP. No format logic in your system. Nothing to change when the formats change. This guide is part of the integrations hub, which also covers SAP BTP, Business Central, Dynamics NAV, Power Automate and other platforms.
Related guides:
- SAP BTP e-invoicing: the same integration as a Cloud Integration iFlow or CAP app.
- SAP Business One ZUGFeRD: the same integration through the Service Layer, for the SME product.
- Dynamics NAV 2018 ZUGFeRD: the same three-object pattern in C/AL.
- ZUGFeRD API toolkit: the format and the endpoints in detail.
- XRechnung API toolkit: Leitweg-ID, KoSIT validation, PDF preview.