Automation MCP Server Blog Pricing
Automation

ZUGFeRD and XRechnung in SAP ECC and S/4HANA: E-Invoicing from ABAP with One API Call

Many German companies run SAP ECC or S/4HANA on-premise. They must send and receive e-invoices now. This guide shows how to create ZUGFeRD invoices with three ABAP classes and one REST API, no add-on and no BTP. It also covers email delivery by the API, XRechnung for public buyers, and checking and importing supplier invoices.

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.com into 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.

CustomerFormatEndpointResult
German business (B2B)ZUGFeRD EN 16931/v1/create/zugferdHybrid PDF/A-3
German business, delivered by the APIZUGFeRD EN 16931/v1/create/zugferd/emailEmail with the PDF attached
German public buyer (B2G)XRechnung/v1/create/xrechnungXML with Leitweg-ID
French businessFactur-X EN 16931/v1/create/facturxHybrid 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 fieldJSON pathNote
VBRK-VBELNinvoice.invoiceNumberBT-1
VBRK-FKDATinvoice.issueDateFormat as yyyy-MM-dd
VBRK-WAERKinvoice.currencyISO code, as stored
T001-BUTXT, T001-STCEG, address from ADRCinvoice.sellerThe seller VAT id is required
KNA1-NAME1, KNA1-STCEG, address from ADRCinvoice.buyerPayer (VBRK-KUNRG) or sold-to (VBRK-KUNAG), your choice
House bank IBAN (T012K, TIBAN)invoice.paymentDetails.paymentAccountIdentifierRecommended for B2B
VBRP-ARKTXlines[].item.namePer line
VBRP-FKIMG / VBRP-NETWRlines[].quantity / lines[].priceDetails.netPriceNet price per unit, after discounts. The API calculates the totals
VBRP-VRKMElines[].unitCodeISO code via UNIT_OF_MEASURE_SAP_TO_ISO
MWST condition of the linelines[].vatInformation.rateKONV on ECC, PRCD_ELEMENTS on S/4HANA
Leitweg-ID (custom field)invoice.buyerReferenceXRechnung 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.

  1. Validate. Send the file to POST /v1/validate/zugferd/report or /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 header X-Invoice-Valid says if the invoice passed. The validationLanguage field sets the language of the report, here German. If you only want the findings as JSON, use /v1/validate/zugferd without /report.
  2. Reply with the report. If the invoice is invalid, email the PDF report to the sender with cl_bcs and 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.
  3. 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_json reads it into the ty_body structure 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

PurposeEndpointInputOutput
Create ZUGFeRDPOST /v1/create/zugferdInvoice JSONHybrid PDF/A-3
Create and email ZUGFeRDPOST /v1/create/zugferd/emailInvoice JSON + email objectEmail sent from your domain
Create XRechnungPOST /v1/create/xrechnungInvoice JSON with Leitweg-IDXRechnung XML
Create and email XRechnungPOST /v1/create/xrechnung/emailInvoice JSON + email objectEmail sent from your domain
Create Factur-XPOST /v1/create/facturxInvoice JSONHybrid PDF/A-3
Validate with PDF reportPOST /v1/validate/zugferd/report, /xrechnung/reportPDF or XML filePDF report, X-Invoice-Valid header
Validate as JSONPOST /v1/validate/zugferd, /xrechnungPDF or XML fileFindings per rule
Extract structured dataPOST /v1/extract/jsonHybrid PDF or XML fileInvoice JSON
Parse plain PDFPOST /v1/parse/jsonAny PDF or imageInvoice JSON + confidence
Extract attachmentsPOST /v1/extract/attachmentsHybrid PDF or XML fileZIP of embedded files
Render XRechnungPOST /v1/render/xrechnung/to/pdfXRechnung XMLReadable 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:

Start free today

Ready to automate your invoices?

Validate, convert and embed compliant e-invoices through one API. Start your 30-day free trial. No credit card required.

GDPR Compliant No credit card required Setup in minutes
Peppol UBL
Factur-X
EN 16931
142 / 142 passed
Compliant
PDF/A-3 embedded