This guide adds ZUGFeRD and XRechnung to abas ERP. You build one small Java app with the abas Essentials SDK (ESDK). It reads abas sales invoices with the typed AJO classes and calls one REST API. The API returns a validated ZUGFeRD PDF. In the email section, the API sends the invoice to the customer itself. In the import section, supplier invoices are checked and matched to the abas vendor and the purchase order. For the format and all endpoints, see the ZUGFeRD API toolkit.
Why abas ERP needs ZUGFeRD now
German manufacturers on abas ERP must send and receive structured e-invoices. All German businesses must accept e-invoices since January 2025. Businesses with more than EUR 800,000 turnover must send them from January 2027. Everyone else follows in January 2028. A PDF printed from an abas form is a plain PDF. The law asks for structured data. ZUGFeRD puts both in one file: a PDF for people, with XML inside for software. The Germany mandate page lists the dates and formats.
abas partners serve many clients. The app reads only standard abas objects: the sales invoice, the customer, the vendor and the purchase order. Settings that differ per client sit in one configuration file. Install the same app in every client and change only that file.
Format updates need no new app version. ZUGFeRD, XRechnung and the EN 16931 rules get new versions over time. The API applies each one on its effective date. Your app sends the same JSON before and after. There is nothing to rebuild or reinstall in abas.
What you need
- abas ERP 22 or later. ESDK apps run on Java 17 there. For older versions, see versions and integration options.
- An ESDK project: Gradle with the ESDK Gradle plugin and access to the abas libraries, as in the ESDK training app.
- The org.json library as a dependency of the app. It builds and reads the JSON.
- Three folders on the abas server: one for created e-invoices, one for staged supplier invoices and one for the configuration file.
- A custom field on the customer for the Leitweg-ID. Only needed for public-sector customers.
- An InvoiceXML account. Create one. You get an API key and 100 free credits. No credit card is needed.
How the integration works
A user saves a sales invoice in abas. An event handler of the ESDK app runs after the save. It reads the invoice with AJO, 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. The app writes the PDF to a folder, or lets the API email it to the customer.
| 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 write these files from the listings in this guide:
einvoice.propertiesandSettings.java: the API key, the seller data, the folders and the field names.InvoiceXmlClient.java: the API calls, JSON bodies and file uploads.InvoiceMapper.java: turns a sales invoice into the JSON body.EInvoiceService.java: creates the file, stores it, sends it by email.InvoiceEventHandler.javaand an entry infop.json: runs the service after a sales invoice is saved.InboundService.java: checks supplier invoices, sends the report back and stages valid ones.
The abas side is standard: the AJO classes sales.Invoice and sales.InvoiceEditor (Verkauf, Rechnung), customer.Customer, vendor.Vendor, purchasing.PurchaseOrder and the AXI2 event handler annotations.
Step 1: Store the API key and call the API
Keep the API key out of the code and out of the abas database. Put it in a properties file on the abas server. Only the operating system user of the abas server should be able to read it. The same file holds the seller data and the folders:
# /etc/abas-einvoice/einvoice.properties (readable only by the abas server user)
api.key=your-api-key
[email protected]
out.dir=/abas/einvoice/out
staging.dir=/abas/einvoice/staging
# Seller: your company as it appears on the invoice
seller.name=Muster Maschinenbau GmbH
seller.vatId=DE123456789
seller.street=Industriestraße 5
seller.city=Karlsruhe
seller.postCode=76131
seller.country=DE
seller.iban=DE02120300000000202051
seller.contact.name=Buchhaltung
seller.contact.phone=+49 721 123450
[email protected]
payment.days=30
# Field names that differ between installations (empty = not used)
field.customer.vatId=
field.customer.country=
field.vendor.vatId=
field.row.taxCode=
# Tax code of an invoice row -> VAT rate in percent (vat.default when no code matches)
vat.default=19
vat.rate.1=19
vat.rate.2=7
Two small classes use the file. Settings loads it once. InvoiceXmlClient calls the API with java.net.http.HttpClient. The upload endpoints take multipart/form-data with one field named file, so the client builds that body by hand. No extra HTTP library is needed.
// Settings.java: reads einvoice.properties once
public final class Settings {
private final Properties props = new Properties();
public Settings() {
Path file = Path.of(System.getProperty("einvoice.config", "/etc/abas-einvoice/einvoice.properties"));
try (Reader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
props.load(reader);
} catch (IOException e) {
throw new UncheckedIOException("Cannot read " + file, e);
}
}
public String get(String key) { return props.getProperty(key, "").trim(); }
public String field(String name) { return get("field." + name); }
}
// InvoiceXmlClient.java: JSON calls and file uploads with java.net.http
public final class InvoiceXmlClient {
private static final String BASE = "https://api.invoicexml.com/";
private final HttpClient http = HttpClient.newHttpClient();
private final String apiKey;
public InvoiceXmlClient(String apiKey) { this.apiKey = apiKey; }
public record Report(boolean valid, byte[] pdf) {}
// POST a JSON body; returns the created file (PDF or XML)
public byte[] postJson(String path, JSONObject body) throws IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body.toString(), StandardCharsets.UTF_8))
.build();
return send(request).body();
}
// Validation report: the verdict is in a header, the body is the PDF report
public Report validateReport(String path, byte[] file, String fileName) throws IOException, InterruptedException {
HttpResponse<byte[]> response = send(upload(path, file, fileName, "de"));
boolean valid = response.headers().firstValue("X-Invoice-Valid").map("true"::equals).orElse(false);
return new Report(valid, response.body());
}
public JSONObject extract(byte[] file, String fileName) throws IOException, InterruptedException {
byte[] body = send(upload("v1/extract/json", file, fileName, null)).body();
return new JSONObject(new String(body, StandardCharsets.UTF_8));
}
// multipart/form-data: optional validationLanguage field, then the field "file"
private HttpRequest upload(String path, byte[] file, String fileName, String language) {
String boundary = "einvoice-" + UUID.randomUUID();
ByteArrayOutputStream out = new ByteArrayOutputStream();
if (language != null) {
text(out, "--" + boundary + "\r\nContent-Disposition: form-data; name=\"validationLanguage\"\r\n\r\n"
+ language + "\r\n");
}
text(out, "--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; filename=\""
+ fileName + "\"\r\nContent-Type: application/octet-stream\r\n\r\n");
out.writeBytes(file);
text(out, "\r\n--" + boundary + "--\r\n");
return HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + apiKey)
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
.POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray()))
.build();
}
private static void text(ByteArrayOutputStream out, String s) {
out.writeBytes(s.getBytes(StandardCharsets.UTF_8));
}
private HttpResponse<byte[]> send(HttpRequest request) throws IOException, InterruptedException {
HttpResponse<byte[]> response = http.send(request, HttpResponse.BodyHandlers.ofByteArray());
if (response.statusCode() != 200) {
// 400 returns JSON with an errors array: the rule id and the JSON path of each problem
throw new IOException("InvoiceXML " + response.statusCode() + ": "
+ new String(response.body(), StandardCharsets.UTF_8));
}
return response;
}
}
The listings show the abas imports and leave out the standard ones. Those come from java.io, java.net, java.net.http, java.nio, java.math, java.time, java.text, java.util and org.json. To read the file from another path, pass -Deinvoice.config=....
Step 2: Map abas fields to JSON
The API takes an EN 16931 invoice as JSON. The invoice data comes from the abas sales invoice and its customer. The seller data comes from the configuration file. This is the mapping:
| abas field | JSON path | Note |
|---|---|---|
Invoice.getIdno() | invoice.invoiceNumber | The invoice number from Verkauf |
Invoice.getDateFrom() | invoice.issueDate | An AbasDate, written as yyyy-MM-dd |
Invoice date plus payment.days | invoice.dueDate | Or the due date field of your installation |
| Configuration | invoice.seller | Name, VAT id, address, IBAN |
Customer.getAddr(), getStreet(), getZipCode(), getTown() | invoice.buyer | The first line of the address is the name |
Field from field.customer.vatId | buyer.vatIdentifier | Left out when not set |
Field from field.customer.country | buyer.postalAddress.country | ISO code; DE when not set |
Row tename | lines[].item.name | The item text of the row |
Row getUnitQty(), getPrice() | lines[].quantity, lines[].priceDetails.netPrice | Net price per unit |
Row he (trade unit) | lines[].unitCode | UN/ECE code: H87 pieces, HUR hours, KGM kilograms |
| Row tax code | lines[].vatInformation.rate | Rate from the vat.rate.* settings |
Customer field yleitwegid | invoice.buyerReference | XRechnung only |
Some field names are not the same in every installation. They sit in the field.* settings. Check the exact names in the field help of the abas screen. A setting can also hold a reference path with ^, such as a field of the linked country record.
You do not send totals or VAT amounts. The API calculates them from the rows. If the numbers do not add up, the API returns an error instead of a file. Wrong prices or rates never reach the customer.
// InvoiceMapper.java: abas sales invoice (Verkauf, Rechnung) -> InvoiceXML JSON
import de.abas.erp.db.schema.customer.Customer;
import de.abas.erp.db.schema.sales.Invoice;
public final class InvoiceMapper {
private final Settings cfg;
public InvoiceMapper(Settings cfg) { this.cfg = cfg; }
// Returns the "invoice" object; callers wrap it as { invoice } or { invoice, email }
public JSONObject build(Invoice invoice) {
if (!(invoice.getCustomer() instanceof Customer customer)) {
throw new IllegalStateException("Invoice " + invoice.getIdno() + " has no customer record");
}
LocalDate issued = LocalDate.parse(new SimpleDateFormat("yyyy-MM-dd").format(invoice.getDateFrom().toDate()));
JSONArray lines = new JSONArray();
for (Invoice.Row row : invoice.table().getRows()) {
BigDecimal quantity = row.getUnitQty();
if (quantity == null || quantity.signum() == 0) continue; // text rows
lines.put(new JSONObject()
.put("quantity", quantity)
.put("unitCode", unitCode(row.getString("he")))
.put("item", new JSONObject().put("name", row.getString("tename")))
.put("priceDetails", new JSONObject().put("netPrice", row.getPrice()))
.put("vatInformation", new JSONObject().put("rate", vatRate(row))));
}
JSONObject seller = new JSONObject()
.put("name", cfg.get("seller.name"))
.put("vatIdentifier", cfg.get("seller.vatId"))
.put("postalAddress", new JSONObject()
.put("line1", cfg.get("seller.street"))
.put("city", cfg.get("seller.city"))
.put("postCode", cfg.get("seller.postCode"))
.put("country", cfg.get("seller.country")));
String country = optional(customer, "customer.country");
JSONObject buyer = new JSONObject()
.put("name", customer.getAddr().split("\n")[0])
.put("vatIdentifier", optional(customer, "customer.vatId")) // null: key is left out
.put("postalAddress", new JSONObject()
.put("line1", customer.getStreet())
.put("city", customer.getTown())
.put("postCode", customer.getZipCode())
.put("country", country == null ? "DE" : country));
JSONObject json = new JSONObject()
.put("invoiceNumber", invoice.getIdno())
.put("issueDate", issued.toString())
.put("dueDate", issued.plusDays(Long.parseLong(cfg.get("payment.days"))).toString())
.put("currency", "EUR") // map the invoice currency for foreign customers
.put("seller", seller)
.put("buyer", buyer)
.put("paymentDetails", new JSONObject().put("paymentAccountIdentifier", cfg.get("seller.iban")))
.put("lines", lines);
// XRechnung fields for public buyers go here (see the XRechnung section)
return json;
}
// Reads a configured field; null when the setting or the value is empty
private String optional(Customer customer, String setting) {
String field = cfg.field(setting);
String value = field.isEmpty() ? "" : customer.getString(field).trim();
return value.isEmpty() ? null : value;
}
private BigDecimal vatRate(Invoice.Row row) {
String field = cfg.field("row.taxCode");
String rate = field.isEmpty() ? "" : cfg.get("vat.rate." + row.getString(field).trim());
return new BigDecimal(rate.isEmpty() ? cfg.get("vat.default") : rate);
}
// UN/ECE Recommendation 20 codes for the trade units you use
private static String unitCode(String unit) {
return switch (unit.trim()) {
case "Stk", "St", "Stck" -> "H87";
case "h", "Std" -> "HUR";
case "kg" -> "KGM";
case "m" -> "MTR";
default -> "C62";
};
}
}
JSONObject.put removes a key when the value is null. So a customer without a VAT id gets no vatIdentifier, and B2B invoices carry no buyerReference. If your rows use price units or row discounts, compute the net price from the net row value divided by the quantity.
Step 3: Create the ZUGFeRD invoice from abas
The service wraps the mapped invoice and posts it. The answer is the finished PDF as bytes. The service also stores it, which step 4 covers.
// EInvoiceService.java: create, store, email
public final class EInvoiceService {
private static EInvoiceService instance;
private final Settings cfg;
private final InvoiceXmlClient api;
private final InvoiceMapper mapper;
private EInvoiceService(Settings cfg) {
this.cfg = cfg;
this.api = new InvoiceXmlClient(cfg.get("api.key"));
this.mapper = new InvoiceMapper(cfg);
}
// One instance per abas server process; the settings are read once
public static synchronized EInvoiceService get() {
if (instance == null) instance = new EInvoiceService(new Settings());
return instance;
}
public byte[] createZugferd(Invoice invoice) throws IOException, InterruptedException {
return api.postJson("v1/create/zugferd", new JSONObject().put("invoice", mapper.build(invoice)));
}
For customers in France, post the same body to v1/create/facturx. Factur-X is the French name of the same standard. The Factur-X API toolkit explains the French rules.
Step 4: Store the PDF when an invoice is saved
The service writes the PDF to the output folder. The file name is the invoice number. If the file is already there, the service skips the invoice. This keeps one e-invoice per abas invoice, even when a user opens and saves it again.
// InvoiceEventHandler.java: runs after a sales invoice is saved
import de.abas.erp.api.gui.TextBox;
import de.abas.erp.axi.event.EventException;
import de.abas.erp.axi.screen.ScreenControl;
import de.abas.erp.axi2.EventHandlerRunner;
import de.abas.erp.axi2.annotation.EventHandler;
import de.abas.erp.axi2.annotation.ScreenEventHandler;
import de.abas.erp.axi2.event.ScreenEvent;
import de.abas.erp.axi2.type.ScreenEventType;
import de.abas.erp.common.type.enums.EnumEditorAction;
import de.abas.erp.db.DbContext;
import de.abas.erp.db.schema.sales.InvoiceEditor;
import de.abas.erp.jfop.rt.api.annotation.RunFopWith;
@EventHandler(head = InvoiceEditor.class)
@RunFopWith(EventHandlerRunner.class)
public class InvoiceEventHandler {
@ScreenEventHandler(type = ScreenEventType.EXIT)
public void screenExit(ScreenEvent event, ScreenControl screenControl, DbContext ctx, InvoiceEditor head)
throws EventException {
if (event.getCommand() == EnumEditorAction.View || event.getCommand() == EnumEditorAction.Delete) {
return; // nothing was saved
}
try {
Path pdf = EInvoiceService.get().createAndStore(head.objectId());
if (pdf != null) ctx.out().println("ZUGFeRD invoice stored: " + pdf);
} catch (Exception e) {
new TextBox(ctx, "E-Rechnung", "Die E-Rechnung wurde nicht erstellt: " + e.getMessage()).show();
}
}
}
// EInvoiceService.java, continued: store the PDF once per invoice
public Path createAndStore(Invoice invoice) throws IOException, InterruptedException {
Path target = Path.of(cfg.get("out.dir"), invoice.getIdno() + "-zugferd.pdf");
if (Files.exists(target)) return null; // already created for this invoice
Files.write(target, createZugferd(invoice));
return target;
}
The trigger. The EXIT screen event runs when the invoice screen is left after a save. At that point head.objectId() returns the stored Invoice. Register the handler in the fop.json of the ESDK project. The entry names the database and group of the invoice screen, the editor mode, the event and the handler as java:de.firma.einvoice.InvoiceEventHandler@einv. The ESDK training app on GitHub shows the format. If the API returns an error, the user sees it in a text box and can fix the invoice right away.
The archive. abas DMS archives the documents that abas prints and the digital files you import. Point the import of your document archive at the output folder. Use the invoice number from the file name as the index. The ZUGFeRD PDF is then linked to the sales invoice, next to the printed form. Send this file to the customer instead of the plain print. It opens like any PDF, and the customer's software reads the XML inside.
Send the invoice by email from the API
The API can also deliver the invoice. It creates the file, validates it and sends it to the customer by email in one call. The app never handles the PDF. In Germany this is a complete delivery: B2B e-invoices have no central platform, and an email with a ZUGFeRD PDF meets the rules.
Verify your sending domain once in the dashboard. Add the domain and publish the three CNAME records it shows. Then any address on that domain can be the sender. The request body is the invoice plus an email object. The recipient is the email address of the abas customer:
public void sendByEmail(Invoice invoice) throws IOException, InterruptedException {
Customer customer = (Customer) invoice.getCustomer();
String number = invoice.getIdno();
JSONObject body = new JSONObject()
.put("invoice", mapper.build(invoice))
.put("email", new JSONObject()
.put("from", cfg.get("email.from")) // address on your verified domain
.put("to", customer.getEmailAddr())
.put("subject", "Ihre Rechnung " + number)
.put("message", "Guten Tag,\n\nim Anhang finden Sie die Rechnung " + number
+ " als ZUGFeRD-PDF.\n\nViele Grüße\nMuster Maschinenbau"));
api.postJson("v1/create/zugferd/email", body); // 200: created, validated, handed to delivery
}
}
Use this route when the API should send. Use the plain endpoint when abas should store the file and send it itself. The email route also exists for XRechnung (/v1/create/xrechnung/email) and Factur-X.
Import incoming supplier invoices
Suppliers now send ZUGFeRD PDFs and XRechnung XML files. Each file goes through three steps before purchasing sees it. A small mailbox reader passes every PDF or XML attachment to the inbound service, with the sender address.
- Validate. Post the file to
POST /v1/validate/zugferd/report(PDF) or/v1/validate/xrechnung/report(XML), withvalidationLanguage=de. The response headerX-Invoice-Validsaystrueorfalse. The body is a PDF report in German. For the findings as JSON, use/v1/validate/zugferdwithout/report. - Return invalid invoices. If the invoice is invalid, email the PDF report to the sender and stop. The report names every broken rule and the field it concerns. The supplier corrects the invoice and sends it again.
- Extract. If the invoice is valid, post the file to
POST /v1/extract/json. The API returns the invoice as JSON, in the same shape as the create request.
// InboundService.java: validate, return the report, extract, stage for purchasing
import de.abas.erp.db.DbContext;
import de.abas.erp.db.schema.purchasing.PurchaseOrder;
import de.abas.erp.db.schema.vendor.Vendor;
import de.abas.erp.db.selection.Conditions;
import de.abas.erp.db.selection.SelectionBuilder;
import de.abas.erp.db.util.QueryUtil;
public final class InboundService {
private final Settings cfg = new Settings();
private final InvoiceXmlClient api = new InvoiceXmlClient(cfg.get("api.key"));
private final DbContext ctx;
private final Mailer mailer; // your SMTP code
public InboundService(DbContext ctx, Mailer mailer) {
this.ctx = ctx;
this.mailer = mailer;
}
public void process(byte[] file, String fileName, String sender) throws Exception {
// 1. Validate: XML files are XRechnung, PDF files are ZUGFeRD
String path = fileName.toLowerCase().endsWith(".xml")
? "v1/validate/xrechnung/report"
: "v1/validate/zugferd/report";
InvoiceXmlClient.Report report = api.validateReport(path, file, fileName);
// 2. Invalid: send the PDF report to the sender and stop
if (!report.valid()) {
mailer.send(sender, "Rechnung " + fileName + ": Prüfung fehlgeschlagen",
"Der Prüfbericht im Anhang nennt die Fehler. Bitte senden Sie uns eine korrigierte Rechnung.",
"Pruefbericht-" + fileName + ".pdf", report.pdf());
return;
}
// 3. Valid: read the data and stage it for purchasing
JSONObject invoice = api.extract(file, fileName).getJSONObject("invoice");
stage(invoice, file, fileName);
}
The service runs as a separate Java program, for example every five minutes as a cron job on the abas server. It opens its own AJO connection with ContextHelper.createClientContext(host, 6550, client, password, "einvoice-inbound") and closes it at the end.
Plain PDFs: some suppliers still send PDFs without XML. Post those to POST /v1/parse/json. The API reads typed, scanned and photographed invoices with AI. It returns the same JSON plus a confidence object with scores from 0.0 to 1.0. Stage high scores directly and mark the rest for a check. Files that are not invoices are rejected with error code 4008.
Prepare the purchase invoice from the JSON
Both endpoints return the same JSON shape. The service uses it to find the abas vendor by the VAT id and the purchase order by its number. Then it writes a staging file with the abas numbers and the invoice data, next to the original file. Purchasing records the invoice against the order in Einkauf with everything at hand.
private void stage(JSONObject invoice, byte[] file, String fileName) throws IOException {
Vendor vendor = findVendor(invoice.getJSONObject("seller").optString("vatIdentifier"));
PurchaseOrder order = findOrder(invoice.optString("purchaseOrderReference"));
JSONObject entry = new JSONObject()
.put("vendor", vendor == null ? null : vendor.getIdno())
.put("purchaseOrder", order == null ? null : order.getIdno())
.put("file", fileName)
.put("invoice", invoice); // includes totals.grandTotalAmount
String name = (vendor == null ? "UNKNOWN" : vendor.getIdno()) + "_" + invoice.getString("invoiceNumber");
Path dir = Path.of(cfg.get("staging.dir"));
Files.writeString(dir.resolve(name + ".json"), entry.toString(2));
Files.write(dir.resolve(name + "_" + fileName), file);
}
// Vendor by VAT id; the field name comes from field.vendor.vatId
private Vendor findVendor(String vatId) {
String field = cfg.field("vendor.vatId");
if (vatId.isEmpty() || field.isEmpty()) return null;
String wanted = vatId.replace(" ", "");
for (Vendor vendor : ctx.createQuery(SelectionBuilder.create(Vendor.class).build())) {
if (wanted.equalsIgnoreCase(vendor.getString(field).replace(" ", ""))) return vendor;
}
return null;
}
private PurchaseOrder findOrder(String number) {
if (number.isEmpty()) return null;
return QueryUtil.getFirst(ctx, SelectionBuilder.create(PurchaseOrder.class)
.add(Conditions.eq(PurchaseOrder.META.idno, number)).build());
}
}
Files without a known vendor start with UNKNOWN, so they are easy to find. Connect the staging folder to the step where your team records supplier invoices. For example, an ESDK infosystem can list the staged files and open the matching purchase order. The original file also goes into the import of your document archive, with the vendor and the order number as the index.
XRechnung from abas ERP for public buyers
German federal, state and municipal buyers need XRechnung, not ZUGFeRD. XRechnung is pure XML with no PDF. Portals such as ZRE and OZG-RE check it against the KoSIT rules. The same app creates it. It needs a few more fields and a different endpoint:
// In InvoiceMapper.build: customers with a Leitweg-ID get the XRechnung fields
String leitwegId = customer.getString("yleitwegid").trim();
if (!leitwegId.isEmpty()) {
json.put("buyerReference", leitwegId);
seller.put("contact", new JSONObject()
.put("name", cfg.get("seller.contact.name"))
.put("phone", cfg.get("seller.contact.phone"))
.put("email", cfg.get("seller.contact.email")));
seller.put("electronicAddress", new JSONObject()
.put("identifier", cfg.get("seller.vatId")).put("schemeId", "9930")); // 9930 = German VAT id
buyer.put("electronicAddress", new JSONObject()
.put("identifier", buyer.optString("vatIdentifier")).put("schemeId", "9930"));
}
// In EInvoiceService: post to "v1/create/xrechnung" when buyerReference is set.
// The response is XML: store it as {idno}-xrechnung.xml in the output folder.
The Leitweg-ID is the routing id that the public buyer gives you. Invoices without it fail rule BR-DE-15. Add a custom field yleitwegid (text, 50 characters) to the customer database. Create it in the variable table, or ship it with the ESDK app as a vartab entry. The mapper copies it into buyerReference. The seller contact and the electronic addresses are also required by the German rules. Scheme 9930 stands for a German VAT id. For Peppol delivery, use the buyer's Peppol participant id and its scheme.
The API returns the XML after the KoSIT checks. Deliver it the way the buyer asks: upload to ZRE or OZG-RE, Peppol, or email where the buyer accepts it. Post the XML to /v1/render/xrechnung/to/pdf to get a readable PDF for your archive. The XRechnung API toolkit covers the format in detail.
abas versions and integration options
abas ERP 22 and later. ESDK apps compile and run on Java 17. The listings run as shown. Install the app with the ESDK Gradle plugin (fullInstall) or with the ESDK App Installer.
Older versions. Before abas ERP 22, ESDK apps target Java 8. Replace java.net.http with HttpURLConnection, the record with a small class and the switch expression with a switch statement. The AJO classes and the JSON body stay the same.
ESDK or FO. abas also has its own scripting language, FO. This guide uses Java because the API client, the JSON and the multipart upload are plain Java code. The ESDK registers the Java handler for the screen event in fop.json and installs it with the app.
REST API. abas ERP also has a REST API under /mw/r/, followed by the client name. It uses basic authentication with the abas password, or OAuth tokens. It returns abas objects as JSON with the media type application/abas.objects+json. A service on another server can read the invoice this way and post the same JSON to InvoiceXML. This call reads customers:
# Read customers (database 0, group 1) with the abas REST API; host and client are examples
curl -u ":$ABAS_PASSWORD" \
-H "Accept: application/abas.objects+json" \
-H "Accept-Language: de" \
'https://abas.firma.local/mw/r/erp/obj/data/0:1?criteria=such=M!N&headFields=nummer,such,namebspr&limit=10'
# The fields are in content.data.erpDataObjects[].head.fields.<name>.value
Sales documents are in database 3 (Verkauf) and purchase documents in database 4 (Einkauf). The Swagger UI of your REST server lists the groups and fields of each database. Writes use an editor: commands/NEW opens one, SetFieldValue actions fill it and a Commit action saves it. Use the REST API for integrations outside abas. Use the ESDK when the e-invoice should be created the moment the invoice is saved.
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 integration is one ESDK app with six small classes: settings, API client, mapper, service, event handler and inbound service. abas keeps its forms, its numbers and its processes. The format rules live in the API, so format updates need no new app version. This guide is part of the integrations hub, which also covers SAP, Microsoft Dynamics, Odoo and other platforms.
Related guides:
- ZUGFeRD API toolkit: the format, the profiles and every endpoint for German B2B.
- XRechnung API toolkit: the Leitweg-ID, the KoSIT rules and the PDF preview.
- SAP Business One ZUGFeRD: the same integration as a C# service on the Service Layer.
- Dynamics AX and D365 Finance ZUGFeRD: the same integration for Microsoft's ERP for larger manufacturers.