Send-via-Peppol badge
Let your users send documents directly via the Peppol network from your website — with a single button. The CDN script badge-sender.js renders a "Deliver using Peppol" button that, when clicked, redirects to the hosted web-sender page where the full send flow is handled.
You need an affiliate token (UUID) which you can obtain in the application under Settings → Affiliate. The token identifies your participant and enables the badge.
How it works
- You render the badge on your page with the affiliate token UUID, the URL of the invoice document (accessible from the internet), and the document format (e.g.
isdoc,pdf,zugferd). - The user clicks the badge — they are redirected to the web-sender page at
https://app.pepposch.eu/web-sender. - The web-sender validates the affiliate token, optionally prompts the user to log in, downloads and auto-detects the document format (ISDOC, ISDOCX, PDF+ISDOC, ZUGFeRD, Factur-X), checks that the recipient is on the Peppol network, and shows a confirmation dialog.
- The user confirms and the document is sent via the Peppol network.
Live preview
// What happens when the user clicks the badge: // // URL mode: // 1. Browser opens: https://app.pepposch.eu/web-sender // with query params: // ?url=https%3A%2F%2Fyour-server.example%2Finvoice.isdoc // &format=isdoc // &affiliateId=YOUR-AFFILIATE-UUID // // Inline / prepareDocument mode: // 1. Browser opens: https://app.pepposch.eu/web-sender // with query params: // ?data=<base64url-deflate-raw-encoded-document> // &format=isdoc // &affiliateId=YOUR-AFFILIATE-UUID // (prepareDocument: badge-sender.js calls the callback BEFORE navigating // and compresses the returned string with CompressionStream/deflate-raw) // // 2. The web-sender page: // a. Validates the affiliateId // b. Checks if the user is logged in (redirects to login if not) // c. Downloads the document from 'url' OR decodes+decompresses 'data' // d. Looks up whether the recipient is registered in Peppol // e. Matches the document sender to one of the user's participants // f. Shows a confirmation card → user clicks "Odeslat přes Peppol" // g. Document is imported + sent via the Peppol network
Option 1 — HTML data-* attributes (simplest)
Place a container element and add data-* attributes directly on the <script> tag. The script auto-initialises on load.
<!-- 1. Place a container where the button should appear --> <div id="peppol-send-btn"></div> <!-- 2. Load the CDN script with data attributes --> <script src="https://app.pepposch.eu/cdn/badge-sender.js" data-target="peppol-send-btn" data-affiliate-id="YOUR-AFFILIATE-UUID" data-url="https://your-server.example/invoice.isdoc" ></script>
Option 2 — JavaScript API
For more control (e.g. dynamic URLs, multiple badges), call pepposhShowSendBadge() directly.
<!-- 1. Container element -->
<div id="peppol-send-btn"></div>
<!-- 2. Load script (no data- attributes needed) -->
<script src="https://app.pepposch.eu/cdn/badge-sender.js"></script>
<!-- 3. Call the API -->
<script>
pepposhShowSendBadge({
targetId: "peppol-send-btn",
affiliateId: "YOUR-AFFILIATE-UUID",
url: "https://your-server.example/invoice.isdoc",
});
</script>Option 3 — Inline data (no public URL required)
If your documents are not publicly accessible (e.g. behind authentication or on a private server), you can embed the document content directly in the badge URL using the inline option / data-inline attribute. The document is compressed with deflate-raw and encoded as base64url.
A typical ISDOC XML invoice (10–20 KB) compresses down to 2–5 KB, resulting in a practical URL length for most browsers. Avoid inline mode for documents larger than ~50 KB.
// Node.js — encode document for inline delivery (no external libraries needed)
// Compression uses Node.js built-in 'zlib'; base64url needs no helper either.
const zlib = require('node:zlib');
/**
* Compress a document buffer and return a base64url string.
* Deflate-raw is supported natively by both Node.js and modern browsers.
*/
function encodeInlineDocument(buffer) {
const compressed = zlib.deflateRawSync(buffer);
return compressed
.toString('base64')
.replace(/\+/g, '-') // base64url: + → -
.replace(/\//g, '_') // / → _
.replace(/=/g, ''); // strip padding
}
// Usage example:
const fs = require('fs');
const inlineData = encodeInlineDocument(fs.readFileSync('invoice.isdoc'));
// inlineData is now safe to embed directly in a URL or HTML attribute.<!-- Inline mode: document content is embedded directly — no server URL needed --> <div id="peppol-send-btn"></div> <script src="https://app.pepposch.eu/cdn/badge-sender.js" data-target="peppol-send-btn" data-affiliate-id="YOUR-AFFILIATE-UUID" data-inline="<!-- YOUR-BASE64URL-ENCODED-DOCUMENT -->" data-format="isdoc" ></script>
// Inline mode via JavaScript API
pepposhShowSendBadge({
targetId: "peppol-send-btn",
affiliateId: "YOUR-AFFILIATE-UUID",
inline: "<!-- YOUR-BASE64URL-ENCODED-DOCUMENT -->",
format: "isdoc",
});
// The 'inline' value is produced server-side:
// zlib.deflateRawSync(buffer).toString('base64')
// .replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'')Option 4 — prepareDocument callback (generate document on click)
Use prepareDocument when there is no pre-existing URL or pre-encoded inline payload — for example when the document is generated dynamically in the browser at the moment the user clicks. The callback receives no arguments and must return the raw document content (e.g. an ISDOC XML string) or a Promise thereof.
Compression: badge-sender.js always tries to compress the returned content using the browser's CompressionStream API (deflate-raw) before building the redirect URL. If the API is unavailable (e.g. older browsers), it falls back to plain base64url encoding. The server transparently handles both variants.
While the document is being prepared the badge is temporarily disabled (greyed out) to prevent double submissions. It re-enables automatically if an error occurs.
// Option 4: prepareDocument callback
// Use when neither a public URL nor pre-encoded inline is available
// (e.g. the document is generated on-the-fly in the browser).
pepposhShowSendBadge({
targetId: "peppol-send-btn",
affiliateId: "YOUR-AFFILIATE-UUID",
format: "isdoc",
// Called when the user clicks the badge.
// Return the raw document content (ISDOC XML, ZUGFeRD XML, …) as a string.
// The script compresses it with the browser's CompressionStream API
// (deflate-raw) and embeds the result in the redirect URL.
// Falls back to plain base64url if CompressionStream is unavailable.
prepareDocument: async function () {
const invoice = await myApp.generateIsdocXml();
return invoice; // raw XML string — badge-sender.js handles compression
},
});
// Combine with pepposhUpdateSendBadge to refresh the callback:
pepposhUpdateSendBadge("peppol-send-btn", {
prepareDocument: async function () {
return await myApp.generateIsdocXml();
},
});// Browser-side compression (badge-sender.js internal — shown for reference)
//
// badge-sender.js always tries to compress inline / prepareDocument content
// using the browser's built-in CompressionStream API before building the URL.
// This keeps the redirect URL as short as possible.
//
// The server (web-sender/preview + import-from-url endpoints) automatically
// detects whether the received 'data' parameter is deflate-raw compressed and
// decompresses it transparently. If decompression fails it treats the payload
// as plain base64url content.
//
// The server-side (Node.js) encoding helper for pre-compressed payloads:
const zlib = require('node:zlib');
function encodeInlineDocument(buffer) {
return zlib.deflateRawSync(buffer)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// Usage:
const encoded = encodeInlineDocument(fs.readFileSync('invoice.isdoc'));
// Pass as: inline: encodedRecipient verification (optional)
To send an invoice over the Peppol network the recipient must be registered on it. Providing recipient identity gives the user immediate feedback on whether sending is possible before they even click the button.
Provide the recipient's EU VAT ID or a country code + registration number. The badge will perform a client-side Peppol lookup immediately after rendering and switch to one of three image variants based on the result:
euVatId, countryCode, and rn fields are used exclusively in the browser to drive the badge image — they are never included in the URL opened when the user clicks the badge. The web-sender page reads the recipient from the document itself.<!-- Recipient verification using EU VAT ID --> <div id="peppol-send-btn"></div> <script src="https://app.pepposch.eu/cdn/badge-sender.js" data-target="peppol-send-btn" data-affiliate-id="YOUR-AFFILIATE-UUID" data-url="https://your-server.example/invoice.isdoc" data-eu-vat-id="CZ12345678" ></script> <!-- Or using country code + registration number --> <script src="https://app.pepposch.eu/cdn/badge-sender.js" data-target="peppol-send-btn" data-affiliate-id="YOUR-AFFILIATE-UUID" data-url="https://your-server.example/invoice.isdoc" data-country-code="CZ" data-rn="12345678" ></script>
pepposhShowSendBadge({
targetId: "peppol-send-btn",
affiliateId: "YOUR-AFFILIATE-UUID",
url: "https://your-server.example/invoice.isdoc",
// Identify the recipient — provide euVatId OR countryCode + rn.
// These fields are used CLIENT-SIDE ONLY to drive the badge image.
// They are never included in the URL opened when the user clicks.
euVatId: "CZ12345678", // EU VAT ID (preferred)
// -- or --
countryCode: "CZ", // ISO 3166-1 alpha-2 country code
rn: "12345678", // Registration / company number
// Badge will changed design based on registration status:
// Checking / error / no info
// Recipient on Peppol
// Recipient NOT on Peppol
});Updating the URL dynamically
When a new invoice is generated (e.g. after a form submit), update the button without re-rendering the whole page:
// After generating a new invoice, update the button URL
// without re-rendering the whole badge:
pepposhUpdateSendBadge(
"peppol-send-btn",
"https://your-server.example/invoice-20240418.isdoc"
);
// Or pass a full options object to change multiple fields:
pepposhUpdateSendBadge("peppol-send-btn", {
url: "https://your-server.example/new-invoice.isdoc",
affiliateId: "YOUR-AFFILIATE-UUID",
format: "isdoc",
});
// In inline mode, update with new encoded data:
pepposhUpdateSendBadge("peppol-send-btn", {
inline: newBase64urlEncodedData,
affiliateId: "YOUR-AFFILIATE-UUID",
format: "isdoc",
});Integration in React, Vue or Angular
Load the script once and call pepposhShowSendBadge after mount. Re-call when the URL changes.
// React / Vue / Angular — render into a ref after mount
import { useEffect, useRef } from "react";
function SendButton({ affiliateId, invoiceUrl }) {
const ref = useRef(null);
useEffect(() => {
if (!invoiceUrl) return;
const script = document.createElement("script");
script.src = "https://app.pepposch.eu/cdn/badge-sender.js";
script.onload = () => {
window.pepposhShowSendBadge({
targetId: "peppol-send-btn",
affiliateId,
url: invoiceUrl,
});
};
if (!document.querySelector('script[src*="badge-sender"]')) {
document.head.appendChild(script);
} else {
window.pepposhShowSendBadge?.({
targetId: "peppol-send-btn",
affiliateId,
url: invoiceUrl,
});
}
}, [affiliateId, invoiceUrl]);
return <div id="peppol-send-btn" />;
}API reference
pepposhShowSendBadge(options)
Renders the send badge inside the element identified by options.targetId. Any existing badge in that element is replaced.
| Parameter | Type | Required | Description |
|---|---|---|---|
| targetId | string | yes | ID of the container DOM element where the badge is rendered. |
| affiliateId | string (UUID) | yes | Your affiliate token UUID from Settings → Affiliate. Acts primarily as a public developer/API key — required for security (the server validates it before processing), and secondarily enables affiliate tracking. |
| url | string | yes* | Public HTTPS URL of the invoice document to send. The badge is disabled when neither url, inline, nor prepareDocument is provided. |
| inline | string | yes* | Base64url-encoded (deflate-raw compressed or plain) document content. Use instead of url when the document has no public URL. Produced server-side with Node.js built-in zlib — no extra library needed. badge-sender.js also tries to compress the payload further in the browser before building the URL. |
| prepareDocument | () => string | Promise<string> | yes* | Callback invoked when the user clicks the badge. Returns the raw document content (ISDOC XML, etc.). badge-sender.js compresses it with the browser CompressionStream API and embeds the result in the redirect URL. Falls back to plain base64url if compression is unavailable. |
| format | string | no | Document format. Accepted values: "isdoc", "isdocx", "pdf" (PDF+ISDOC), "zugferd", "factur-x". The server auto-detects the format from the downloaded bytes; this parameter is primarily used as a display hint. Default: "isdoc". |
| euVatId | string | no | Recipient EU VAT ID (e.g. "CZ12345678"). Client-side only — drives the badge image variant via a Peppol lookup; never sent to the server when the badge is clicked. |
| countryCode | string | no | Recipient country code (ISO 3166-1 alpha-2, e.g. "CZ"). Client-side only — used together with rn for the Peppol lookup when euVatId is not provided. |
| rn | string | no | Recipient registration / company number. Client-side only — used together with countryCode for the Peppol lookup. |
| width | string | number | no | Optional width of the badge image (e.g. 200 or "200px"). |
pepposhUpdateSendBadge(targetId, urlOrOptions)
Updates an already-rendered badge without re-creating it. Pass a plain URL string or a partial options object.
| Parameter | Type | Description |
|---|---|---|
| targetId | string | ID of the container element holding the badge. |
| urlOrOptions | string | object | New URL string, or a partial options object (same fields as pepposhShowSendBadge). |
data-* attribute reference
When using the HTML attribute approach, add these to the <script> tag:
| Attribute | JS option | Description |
|---|---|---|
| data-target | targetId | ID of the target container element. |
| data-affiliate-id | affiliateId | Affiliate token UUID. |
| data-url | url | URL of the invoice document. |
| data-inline | inline | Base64url-encoded (deflate-raw) document content. Use instead of data-url when no public URL is available. |
| data-format | format | Document format: "isdoc" (default), "isdocx", "pdf", "zugferd", "factur-x". |
| data-eu-vat-id | euVatId | Recipient EU VAT ID for Peppol lookup (e.g. "CZ12345678"). Client-side only — not forwarded to the server. |
| data-country-code | countryCode | Recipient country code (ISO 3166-1 alpha-2). Used with data-rn. Client-side only. |
| data-rn | rn | Recipient registration / company number. Used with data-country-code. Client-side only. |
| data-width | width | Optional badge image width. |
Supported document formats
The server auto-detects the actual format from the downloaded file content (binary signature or XML namespace). The format parameter is used as a display hint for the badge UI.
| format value | File type | Extension | Notes |
|---|---|---|---|
| isdoc | ISDOC XML | .isdoc | Czech invoice XML. Default format. |
| isdocx | ISDOCX archive | .isdocx | ZIP archive containing ISDOC XML with optional attachments. |
| PDF + ISDOC | PDF document with an embedded ISDOC XML as an attachment. | ||
| zugferd | ZUGFeRD | PDF document with embedded CII XML (ZUGFeRD 2.x / EN 16931). | |
| factur-x | Factur-X | PDF document with embedded Factur-X CII XML. Identical container to ZUGFeRD. |
Document URL requirements
- Must be publicly reachable via HTTPS (HTTP is also accepted but not recommended).
- Must not point to a private IP (
127.*,10.*,192.168.*, etc.) — such URLs are blocked by the server. - The document must contain valid invoice data that the user's account is authorised to send.
- The URL should remain accessible for at least a few minutes after the button is rendered (the server downloads it when the user confirms).
Behavior notes
- The badge is disabled (greyed out, not clickable) when
url,inline, andprepareDocumentare all missing, or whenaffiliateIdis missing. - When
prepareDocumentis set, the badge intercepts the click, calls the callback, compresses the result and then opens the web-sender page in a new tab. The badge is temporarily disabled while the document is prepared. If the callback throws, the badge re-enables. - The badge language (alt text and badge image) is detected automatically from the browser's
navigator.language. Supported: en, cs, de, sk, pl, hu, fr, es, it, ro, pt, hr, sl, uk, nl, da, sv, no, fi, bg, lt, lv, et, el, ru, be, sr, bs, mk, al, mt, ga, cy, is, lu, ua, vi. Falls back toen. - The badge renders a
<a>element that opens in a new tab (target="_blank"). - One shared
<script>include can power any number of badges on the same page. - The script is safe to include in the
<head>or at the end of<body>; it waits forDOMContentLoadedbefore auto-initialising. - Styles are injected once (keyed by a unique ID) so multiple includes cause no duplication.