Browser-side JavaScript examples for creating, validating, and extracting ZUGFeRD electronic invoices using the InvoiceXML API. Self-contained HTML files using vanilla JavaScript with native fetch and FormData. Compatible with modern browsers: Chrome, Firefox, Safari, Edge. No build step, no framework, no dependencies.
For background on the ZUGFeRD standard itself (what it is, profiles, legal status), see the main repository README.
API keys embedded directly in browser-side JavaScript are visible to every visitor through View Source or DevTools. Anyone can extract the key and use it against your InvoiceXML quota.
These examples are appropriate for:
- Local prototyping and testing
- Internal tools behind your company's authentication
- Single-user desktop apps wrapped in Electron or Tauri
For public-facing production websites, route requests through your own backend so the API key stays server-side. See the Node.js examples or PHP examples for server-side integration patterns.
Sign up and generate a key here:
→ https://www.invoicexml.com/account/authentication
Set apiKey in the examples to the raw key only, without the Bearer prefix. If your account page shows the full header value (e.g. Bearer ixml_a1b2c3...), copy only the part after Bearer . The code adds the prefix itself when building the Authorization header.
- Any modern browser:
- Chrome 95+
- Firefox 90+
- Safari 14+
- Edge 95+
- An InvoiceXML API key
- Optional: a local web server (
python -m http.server,npx serve, etc.) for testing. The examples also work by opening the.htmlfiles directly viafile://.
| File | Operation | API endpoint |
|---|---|---|
create.html |
Build a ZUGFeRD PDF/A-3 invoice with embedded EN 16931 XML | POST /v1/create/zugferd |
validate.html |
Validate a ZUGFeRD file against schematron rules | POST /v1/validate/zugferd |
extract-json.html |
Extract ZUGFeRD invoice data as JSON | POST /v1/extract/json |
extract-xml.html |
Extract the raw factur-x.xml from a ZUGFeRD PDF |
POST /v1/extract/xml |
ai-convert.html |
(Experimental) Convert a plain PDF to ZUGFeRD with AI | POST /v1/transform/to/zugferd |
Each file is a complete, runnable HTML page. Open it in a browser, paste your API key into the <script> block, and click the button.
<button id="run">Generate invoice</button>
<script>
const payload = {
invoice: {
invoiceNumber: 'MIN-001',
issueDate: '2026-05-18',
currency: 'EUR',
seller: { name: 'Acme', vatIdentifier: 'DE123456789', /* ... */ },
buyer: { name: 'Globex SAS', /* ... */ },
paymentDetails: { paymentAccountIdentifier: 'DE89370400440532013000' },
lines: [{ quantity: 10, priceDetails: { netPrice: 150.00 },
vatInformation: { rate: 19.00 }, item: { name: 'Senior consulting' } }],
},
};
document.getElementById('run').addEventListener('click', async () => {
const response = await fetch('https://api.invoicexml.com/v1/create/zugferd', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + apiKey,
},
body: JSON.stringify(payload),
});
const blob = await response.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'invoice-zugferd.pdf';
a.click();
});
</script>The PDF is downloaded directly to the user's machine via URL.createObjectURL and a triggered <a download>.
Full example: create.html | API reference
<input type="file" id="file" accept="application/pdf">
<button id="run">Validate</button>
<pre id="output"></pre>
<script>
document.getElementById('run').addEventListener('click', async () => {
const file = document.getElementById('file').files[0];
const form = new FormData();
form.append('file', file);
form.append('version', '2.3.2');
form.append('profile', 'extended');
const response = await fetch('https://api.invoicexml.com/v1/validate/zugferd', {
method: 'POST',
headers: { Authorization: 'Bearer ' + apiKey },
body: form,
});
document.getElementById('output').textContent = await response.text();
});
</script>The file is read directly from the <input type="file"> element. No FileReader or Blob conversion needed: FormData.append(file) accepts the File object directly.
Full example: validate.html | API reference
Useful for displaying invoice details in a web UI, populating form fields from an uploaded ZUGFeRD PDF, or building drag-and-drop invoice review tools.
const form = new FormData();
form.append('file', fileInput.files[0]);
const response = await fetch('https://api.invoicexml.com/v1/extract/json', {
method: 'POST',
headers: { Authorization: 'Bearer ' + apiKey },
body: form,
});
// The invoice document sits under the "invoice" key of the response.
const { invoice } = await response.json();Full example: extract-json.html | API reference | Sample response
Returns the raw factur-x.xml payload (UN/CEFACT Cross-Industry Invoice syntax). The HTML example also generates a download link so the user can save the XML file locally.
Full example: extract-xml.html | API reference
Experimental feature. Human verification required before any production use.
AI extraction can make subtle mistakes that automated validators may not catch: wrong tax category codes, transposed amounts, missing seller VAT identifiers, incorrect currency formatting. Always review the output PDF before sending it to a customer or tax authority. See the AI conversion notes in the main README.
Full example: ai-convert.html | API reference
The patterns below assume the API key is held server-side and proxied through your backend. Replace /api/zugferd/... with your own proxy endpoints.
function CreateZugferdButton() {
const [status, setStatus] = useState('');
async function handleClick() {
setStatus('Generating...');
const response = await fetch('/api/zugferd/create', { method: 'POST' });
const blob = await response.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'invoice-zugferd.pdf';
a.click();
setStatus('Done');
}
return <button onClick={handleClick}>Generate invoice</button>;
}<template>
<button @click="handleClick">Generate invoice</button>
<p>{{ status }}</p>
</template>
<script setup>
import { ref } from 'vue';
const status = ref('');
async function handleClick() {
status.value = 'Generating...';
const response = await fetch('/api/zugferd/create', { method: 'POST' });
const blob = await response.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'invoice-zugferd.pdf';
a.click();
status.value = 'Done';
}
</script><script>
let status = '';
async function handleClick() {
status = 'Generating...';
const response = await fetch('/api/zugferd/create', { method: 'POST' });
const blob = await response.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'invoice-zugferd.pdf';
a.click();
status = 'Done';
}
</script>
<button on:click={handleClick}>Generate invoice</button>
<p>{status}</p>Use HttpClient with responseType: 'blob':
this.http.post('/api/zugferd/create', payload, { responseType: 'blob' })
.subscribe(blob => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'invoice-zugferd.pdf';
a.click();
});- CORS errors in the browser console: if the browser blocks the request, route through your own backend (proxy) instead of calling the API directly from the browser.
- API key visible in DevTools: this is expected when the key is embedded in client-side code. For anything beyond local prototyping, move the call to a server-side proxy.
HTTP 401 Unauthorized: API key missing or invalid. Generate one at invoicexml.com/account/authentication. A frequent cause: settingapiKeyto the wholeBearer xxxvalue, which sendsBearer Bearer xxx. Set the raw key only.HTTP 400 Bad Requeston Create: a required field is missing or malformed. Frequent causes:IssueDatenot in ISO format (YYYY-MM-DD),Currencynot in ISO 4217 (EUR,USD), country codes not in ISO 3166-1 alpha-2 (DE,FR).- File input always empty:
fileInput.files[0]isundefineduntil the user actually picks a file. Always check before reading. - Mixed content warnings: if your page is served over
https://, all API calls must also behttps://. The examples usehttps://api.invoicexml.comso this is fine, just do not change it tohttp://. file://protocol limitations: opening the HTML directly viafile://works for these examples but breaks some browser APIs in stricter modes. Use a local server (python -m http.server,npx serve) if you hit issues.