Mapping versions
How an ERP's field names become our invoice, and how versions change.
- In the sandbox
In plain words
A mapping translates one ERP's field names into our invoice model, which is how an error can name the field in the client's own export. When a client's field names change we add a new version and keep the old one, so we can go back. Business Central and SAP Business One are mapped, each from the ERP's published shape and not yet from a client's own export.
A mapping turns one ERP export into our canonical invoice. When a client's field names change, we add a new version and move the live pointer. The old version stays, so the pointer can move back.
What exists today
Two ERPs are mapped: Business Central (the API v2.0 sales invoice) and SAP Business One (an invoice or credit memo as the Service Layer returns it). Each has a live version, and older versions stay.
Note
Both mappings were written from the ERP's published shape and tested on invented exports, because no client export exists yet. A client whose export uses other names gets a new version. Nothing here asks the client to change the ERP.
An export goes to POST /validate or POST /invoices, with connector and export in place of document.
Send an export
POST /validate takes connector (business-central or sap-b1) and export (the export object) in place of document; POST /invoices takes the same two fields. Fields the mapping does not know are ignored. The report says which version ran, in mapping_version. The request is validate-export-ok.json, an export with invented parties.
curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
--data-binary @validate-export-ok.jsonimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-sandbox-eu.eurinvoice.com/validate"))
.header("Authorization", "Bearer <your-api-key>")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofFile(Path.of("validate-export-ok.json")))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}import { readFile } from 'node:fs/promises';
const response = await fetch('https://api-sandbox-eu.eurinvoice.com/validate', {
method: 'POST',
headers: {
Authorization: 'Bearer <your-api-key>',
'Content-Type': 'application/json',
},
body: await readFile('validate-export-ok.json'),
});
console.log(response.status);
console.log(await response.text());{
"valid": true,
"route": "DE-XRECHNUNG",
"documents": [
{
"sha256": "3e7f648bdc6c6d6f5ad78cd356b39c3020595bb7f0896b78a8510ec6969db317",
"kind": "xrechnung-ubl",
"content_base64": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgi... (5,900 characters, shortened for these docs)"
}
],
"layers": [
{
"findings": [],
"passed": true,
"layer": "schema"
},
{
"findings": [],
"passed": true,
"layer": "mapping"
},
{
"findings": [],
"passed": true,
"layer": "pre-check"
},
{
"findings": [],
"passed": true,
"layer": "kosit"
}
],
"mapping_version": "v2"
}Try another version
mapping_version in the request picks a version for that call. The pointer does not move. A version that does not exist answers 400. The request is validate-export-bad-version.json, the same export with "mapping_version": "v9".
curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
--data-binary @validate-export-bad-version.jsonimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-sandbox-eu.eurinvoice.com/validate"))
.header("Authorization", "Bearer <your-api-key>")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofFile(Path.of("validate-export-bad-version.json")))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}import { readFile } from 'node:fs/promises';
const response = await fetch('https://api-sandbox-eu.eurinvoice.com/validate', {
method: 'POST',
headers: {
Authorization: 'Bearer <your-api-key>',
'Content-Type': 'application/json',
},
body: await readFile('validate-export-bad-version.json'),
});
console.log(response.status);
console.log(await response.text());{
"detail": "That mapping version does not exist. The live pointer was left unchanged.",
"type": "https://eurinvoice.com/problems/bad-request",
"title": "The request could not be read",
"status": 400
}Find the field in the ERP
A finding names the ERP's field, not our canonical one. A unit the mapping does not know is ours to fix: we add it to the unit table and cut a new version. The request is validate-export-bad-unit.json, where Kiste is not in the table.
curl -X POST "https://api-sandbox-eu.eurinvoice.com/validate" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
--data-binary @validate-export-bad-unit.jsonimport java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
public class Example {
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-sandbox-eu.eurinvoice.com/validate"))
.header("Authorization", "Bearer <your-api-key>")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofFile(Path.of("validate-export-bad-unit.json")))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
}
}import { readFile } from 'node:fs/promises';
const response = await fetch('https://api-sandbox-eu.eurinvoice.com/validate', {
method: 'POST',
headers: {
Authorization: 'Bearer <your-api-key>',
'Content-Type': 'application/json',
},
body: await readFile('validate-export-bad-unit.json'),
});
console.log(response.status);
console.log(await response.text());{
"valid": false,
"route": "DE-XRECHNUNG",
"layers": [
{
"findings": [],
"passed": true,
"layer": "schema"
},
{
"findings": [
{
"code": "BR-CL-23",
"field": "salesInvoiceLines[0].unitOfMeasureCode",
"fix_hint": "Add the ERP's unit to the client's unit mapping table (for example 'Std.' to HUR).",
"who_fixes": "us",
"source": "mapping",
"message": "A unit of measure on a line is not recognised. We are adding it to your mapping."
}
],
"passed": false,
"layer": "mapping"
},
{
"findings": [],
"passed": true,
"layer": "pre-check"
}
],
"mapping_version": "v2"
}Missing data is the partner's to fix. Its finding has who_fixes set to erp and names the ERP field to complete.
How a change ships
- We copy the live version to a new version and change it. The old one stays.
- We send the client's sample exports with
mapping_versionset to the new version, while the old one stays live. - We move the live pointer to the new version.
- If something is wrong, we move it back.