Sending Peppol documents in Germany for the companies you onboard
Getting started with Peppol e-invoicing for German companies you onboard from your own software platform: register and verify the company, send Peppol invoices and credit notes, go live. VAT numbers under scheme 9930, XRechnung in UBL and CII alongside Peppol BIS 3, and Leitweg-IDs for public authorities.
This guide walks through everything needed to exchange Peppol documents for a company registered in Germany, assuming you are integrating Recommand into your own product and onboarding German companies as your users, and that the company only sends documents over Peppol. Change any of the three answers at the top of the page to get the guide for a different situation.
What you are building
You are onboarding companies that are not your own: one Recommand team, and a company inside it for every customer you put on the network. Your customers never need a Recommand account. They see your interface, and Recommand stays behind your API calls.
That shape has a few consequences worth knowing before you write code:
- One team, many companies. There is no limit on companies per team, and billing is per team with the document volume of all companies pooled together, so the more companies you onboard the lower your price per document. See pricing per team.
- Every company is registered and verified individually. Peppol identifies companies, not platforms. Each company gets its own Peppol address and its own authorisation record.
- Verification is taken care of. Recommand hands you a URL that the company's authorised representative opens to confirm their identity. You present or forward that link; you never need to handle identity documents yourself. If you prefer to handle verification yourself, reach out to us at support@recommand.eu, we have a few other flows we can set up for you.
- You can run the whole flow under your own brand. The API is designed for white-label use, see can I whitelabel Recommand.
The Recommand dashboard shows the same teams, companies and documents your API calls produce, which is the quickest way to see what a customer is looking at while you are debugging.
Only setting up your own company?
If the only company you will register is your own, switch the first answer above to One company for the shorter version of this guide. The endpoints are the same; there is simply less to organise.
Peppol in Germany
Germany has no central platform: e-invoices travel directly between the parties, over Peppol, email or EDI. What the law prescribes is the content, a structured invoice following EN 16931, not the channel.
What is specific to Germany:
- A B2B mandate in phases. Every German business has had to be able to receive structured e-invoices since 1 January 2025. Issuing them becomes mandatory from 1 January 2027 for businesses with a prior-year turnover above EUR 800,000, and from 1 January 2028 for the rest, with statutory exemptions.
- XRechnung alongside Peppol BIS 3. XRechnung is the German specialisation (CIUS) of EN 16931, in UBL and in CII. German companies registered through Recommand receive both Peppol BIS 3 and XRechnung, and Recommand can write and read both.
- The VAT number is the Peppol address. German businesses are published
under scheme
9930with their VAT number (USt-IdNr.), so a German Peppol address looks like9930:DE123456788. - Public authorities are addressed by their Leitweg-ID. Invoices to German
public authorities go to scheme
0204, and the same Leitweg-ID has to appear in the invoice as buyer reference (BT-10). - German sellers have extra mandatory fields. Peppol BIS 3 and XRechnung both require a German seller to state payment instructions and a contact phone number and email address.
Create your team and API credentials
- Sign up at app.recommand.eu/signup. The team you get is the container for every company you will onboard.
- Create an API key on the API keys page. Note the key, the secret and your team ID.
- Check the credentials with a request that needs no data of its own:
curl -X GET https://app.recommand.eu/api/core/auth/verify \
-u key_xxx:secret_xxxIt answers {"success": true} when the key and secret are accepted, and 401
when they are not, so it tells you about your credentials and nothing else.
All endpoints in this guide live under https://app.recommand.eu/api/v1 and
accept HTTP Basic authentication with the key as username and the secret as
password. If you would rather not store a long-lived secret, JWT API keys and
OAuth2 with a JWT assertion are available too, see the
authentication guide.
Try it safely first
Build the whole flow against a playground team before you touch production. Playgrounds look and behave like production teams, but nothing is delivered over the real Peppol network, there are no SMP registrations, no subscription checks and no billing.
Create one from the team switcher at the top of the dashboard: Add playground, give it a name, and you are switched into it. There is no limit on how many you create.
Everything that follows in this guide is identical there: same endpoints, same validation, same webhooks (triggered by simulated inbound delivery). Register a company in the playground and use it as both sender and recipient to see a document arrive.
Three things worth knowing before you start:
POST /:companyId/generatetakes the same body as the send endpoint minus the email delivery options, which have no meaning when nothing is delivered, and returns the exact XML that sending would produce, fully validated, without transmitting, storing or billing it. Raw XML is not accepted here: there is nothing to generate from a document you already have. It also returns the resolveddocumentType,doctypeIdandprocessId, which is the quickest way to check that your country-specific fields map to the format and process you expect.- Failure addresses. In a playground that is not connected to the Peppol Test
Network, sending to
404:404or0208:1234567894always fails, so you can exercise your error handling. Any other unregistered recipient is skipped without an error. - The Peppol Test Network. For genuine end-to-end tests with real counterparties, tick Use Peppol Test Network when you create the playground. It then uses dedicated test access point and SMP endpoints while staying fully separated from production. The setting cannot be changed after creation, so make a second playground if you want both.
More detail in the getting started guide and how do I use the playground environment.
Register the company
Create one company per customer with the create company endpoint. Registration on the Peppol network happens as part of this call: identifiers and document types are set up for you, based on the company's country.
const auth =
"Basic " + Buffer.from("key_xxx:secret_xxx").toString("base64");
const response = await fetch("https://app.recommand.eu/api/v1/companies", {
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json" },
body: JSON.stringify(company),
});
const result = await response.json();
if (!result.success) throw new Error(JSON.stringify(result.errors));
const companyId = result.company.id;
const verificationUrl = result.verificationUrl; // hand this to your usercurl -X POST https://app.recommand.eu/api/v1/companies \
-u key_xxx:secret_xxx \
-H "Content-Type: application/json" \
-d @company.jsonThe response carries a verificationUrl straight away. Keep it: the next step is
to put it in front of the company's representative.
Only register companies you are allowed to act for
Register the companies that use your platform, not the companies they invoice. Customers and suppliers manage their own Peppol registration; adding them causes registration conflicts. See managing companies.
The exact identifier fields to send depend on the country, which is what the next
section covers. If you would rather create identifiers and document types
yourself instead of accepting the country defaults, pass
skipDefaultCompanySetup: true and use the company
identifiers and
company document
types endpoints.
Three identifiers that are easy to mix up
A German invoice involves three different identifiers. Only the first one is a Peppol address:
| Identifier | What it is for | Where it goes |
|---|---|---|
| Participant identifier | The company's address on the Peppol network | A company identifier, e.g. 9930:DE123456788 |
| Legal registration identifier | The register entry, e.g. HRB 12345 B, shown on the invoice (BT-30) | enterpriseNumber, with an optional enterpriseNumberScheme |
| Buyer reference (BT-10) | The buyer's routing reference; for an authority, its Leitweg-ID | buyerReference on the document |
German identifiers and Peppol address
| Field | German value |
|---|---|
country | "DE" |
vatNumber | DE + 9 digits, e.g. DE123456788 |
enterpriseNumber | Optional: the commercial register number |
enterpriseNumberScheme | Optional: leave it out unless the buyer expects one |
{
"name": "Beispiel GmbH",
"address": "Musterstraße 1",
"postalCode": "10115",
"city": "Berlin",
"country": "DE",
"enterpriseNumber": "HRB 12345 B",
"vatNumber": "DE123456788"
}The VAT number is checked against the German format and its check digit, and
registered as the company's Peppol identifier: 9930: followed by the VAT
number. The register number is never registered as a Peppol identifier.
Without a VAT number
A company without a VAT number gets no identifier by default. Following the German identifier guideline, add one of these yourself with the create identifier endpoint or in the dashboard:
0088: the company's GS1 Global Location Number (GLN), 13 digits9918: the company's IBAN
Both are checked for their check digits.
Public authorities: the Leitweg-ID
A German public authority receives invoices under scheme 0204 with its
Leitweg-ID, for example 0204:991-33333TEST-33. Only add a 0204 identifier
for a public authority that receives through Recommand, using the exact
Leitweg-ID it was assigned; its check digits are verified. The older Leitweg-ID
scheme 9958 is deprecated on Peppol and is refused.
Which identifier you send from
A company sends from the identifier with the lowest scheme, but never from a
Leitweg-ID: that one only names an authority's invoice reception. An invoice to
a Leitweg-ID always goes out under the company's VAT number (9930) or, without
one, its GLN (0088), the sender identifiers the federal invoice portals list.
Registering for sending only
Because you are only looking to send invoices or other documents, register the company without recipient registration:
set isSmpRecipient to false (or leave the checkbox unticked in the
dashboard).
{
"isSmpRecipient": false
}What that means:
- The company is not published as a recipient on an SMP, so nothing is delivered to it over Peppol through your integration.
- Registration succeeds even when the company already receives its documents through another Peppol provider. The other Peppol provider will remain in charge for processing received documents for this company.
- Nothing changes for sending: outgoing documents leave through the access point as normal.
Adding receiving later
You can flip isSmpRecipient to true on an existing company at any time.
Recommand then publishes it as a recipient and registers the document types for
its country. That registration is exclusive, so the company has to be
deregistered at its current provider first.
Verify the company
A company cannot exchange documents on Peppol until an authorised representative
has confirmed their identity. The company object exposes this as isVerified,
and it stays false until the check is done.
For German companies the flow is short:
- Open the verification URL (from the create-company response, the dashboard, or a fresh one from the verify company endpoint).
- The representative fills in their name and completes the identity check.
isVerifiedflips totrueand the company is published on the Peppol network.
In some cases manual verification by our team will be required.
If that's the case, isVerified will remain false until this manual verification is completed.
The user is informed of this in the verification process.
Building verification into your onboarding
With one company you would click through this once. With many, verification is part of the flow you build: every company you register needs its own, and it is the step most likely to leave a customer stuck halfway.
Show the URL immediately. The create-company response already carries
verificationUrl, so no extra call is needed. Put it in front of the user
while they are still in your onboarding.
Ask for a fresh one when the moment has passed. Links get lost, and companies you created earlier never had one shown. The verify company endpoint starts a new verification session:
curl -X POST https://app.recommand.eu/api/v1/companies/{companyId}/verify \
-u key_xxx:secret_xxxThe company.verification webhook fires
when verification reaches a final state: verified, rejected or error. A
company that comes back rejected or error and is not surfaced anywhere sits
silently unusable. See working with webhooks.
Respect isVerified in your own UI. Do not let a user press send for a
company that is not verified yet; this will result in an error. You should inform the user what is missing instead.
Re-verify after identifier changes. Updating a company's vatNumber or
enterpriseNumber resets isVerified to false. Check the field after an
update and present a new verificationUrl if it flipped.
The full mechanics are in the company verification guide.
Pick the document format
Send Peppol BIS 3 UBL unless the recipient asks for XRechnung. When a recipient is only registered for XRechnung, Recommand picks it automatically; to ask for it explicitly, name its document type on the send request:
| What you send | doctypeId |
|---|---|
| Invoice or credit note (default) | not needed, defaults to Peppol BIS 3 UBL |
| XRechnung 3.0 UBL invoice | urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0::2.1 |
| XRechnung 3.0 UBL credit note | urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0::2.1 |
| XRechnung 3.0 CII invoice or credit note | urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100::CrossIndustryInvoice##urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0::D16B |
The document you post is the same for every format. XRechnung does require a few fields that are optional elsewhere, and a send that misses one is refused with the full list before anything is transmitted:
buyerReference(BT-10), unless it is filled in from a Leitweg-ID (see below)paymentMeans, at least one payment instruction- the seller's
phoneandemail, and the seller's and buyer's city and postal code - for a CII credit note,
paymentTerms
Peppol BIS 3 applies the same payment instruction and seller contact rules to every German seller, so fill them in whichever format you send.
Raw XRechnung XML can be sent as it is with documentType: "xml": the document
type and process are read from the document, for UBL and CII alike. XRechnung
3.0 extension documents and earlier XRechnung versions are sent under the
identifier the document declares, but are not read.
Invoicing a public authority
Address the authority by its Leitweg-ID, as 0204: followed by the Leitweg-ID.
German public authorities require XRechnung, so a document to a Leitweg-ID is
written as XRechnung by default, also when the authority is registered for
Peppol BIS 3 as well. The XRechnung fields listed above are therefore required.
The same Leitweg-ID has to be the invoice's buyer reference (BT-10), so leave
buyerReference out and Recommand fills it in. A different buyerReference is
refused, because the authority would reject the invoice; put order numbers in
purchaseOrderReference instead.
{
"recipient": "0204:991-33333TEST-33",
"documentType": "invoice",
"document": {
"invoiceNumber": "RE-2026-001",
"purchaseOrderReference": "4500012345",
"buyer": { "name": "Bundesamt für Beispiele", "street": "Amtsweg 2", "city": "Bonn", "postalZone": "53113", "country": "DE" },
"paymentMeans": [{ "paymentMethod": "credit_transfer", "iban": "DE89370400440532013000", "reference": "RE-2026-001" }],
"lines": [{ "name": "Beratung", "quantity": "1", "netPriceAmount": "100.00", "vat": { "category": "S", "percentage": "19.00" } }]
}
}Before a first invoice to a federal authority
Each authority and portal sets its own acceptance terms. Confirm the format the authority accepts, and check that the recipient is reachable with the verify document support endpoint before the first send.
Send a document
One endpoint sends everything: POST /:companyId/send. The companyId is the
sender, recipient is the Peppol address of the receiver, and document is the
invoice or credit note as JSON. Recommand will validate the document and generate the XML.
Already producing UBL or CII XML?
Raw XML sending is supported as well: set documentType to xml and pass the
document string in document, along with the correct doctypeId. See working
with raw UBL.
const response = await fetch(
`https://app.recommand.eu/api/v1/${companyId}/send`,
{
method: "POST",
headers: { Authorization: auth, "Content-Type": "application/json" },
// The body below, with doctypeId and countrySpecific where the country
// needs them.
body: JSON.stringify(sendRequest),
}
);
const result = await response.json();
if (!result.success) {
// result.errors is keyed by field path, e.g. { "buyer.vatNumber": [...] }
}curl -X POST https://app.recommand.eu/api/v1/{companyId}/send \
-u key_xxx:secret_xxx \
-H "Content-Type: application/json" \
-d @send-invoice.json{
"recipient": "0208:0123456789",
"documentType": "invoice",
"document": {
"invoiceNumber": "INV-2026-001",
"issueDate": "2026-08-17",
"dueDate": "2026-09-16",
"currency": "EUR",
"buyer": {
"name": "Customer Company",
"street": "Customer Street 1",
"city": "Antwerp",
"postalZone": "2000",
"country": "BE",
"vatNumber": "BE0987654321"
},
"paymentMeans": [{ "iban": "BE68539007547034" }],
"lines": [
{
"name": "Consulting Services",
"quantity": "10.00",
"unitCode": "HUR",
"netPriceAmount": "100.00",
"vat": { "category": "S", "percentage": "21.00" }
}
]
}
}The seller block is filled in from the company when you leave it out, which is usually what you want: it keeps the company's registered identifiers and the document in agreement.
Things worth wiring up while you are here:
- Validation errors. Outgoing documents are always validated. A
success: falseresponse witherrorskeyed by field path is a document your recipient would have rejected, so surface it to the user who typed the data. - Delivery outcomes.
success: trueconfirms the send request succeeded. CheckdeliveryStatusanddeliveries, and subscribe to delivery status webhooks for outcomes reported later. - Email fallback. Pass
email.towithwhen: "on_peppol_failure"to fall back to email on Peppol failure, including one reported after the send returns, or send withrecipient: nullfor email-only delivery. See email delivery and notifications. - PDFs.
pdfGeneration.enabledattaches a generated PDF of the document. You can also attach an existing PDF (or other files) viaattachments; see adding attachments.
For a full field reference, see sending invoices and sending credit notes.
Going live
Before you switch your first real customer over, walk this list:
- A valid subscription in production. Playgrounds skip the subscription check; production does not.
- Verification handled in your UI. Show the
verificationUrlat the right moment, make it forwardable, and handle thecompany.verificationwebhook so a company that comes backrejectedorerrordoes not sit silently unusable. isVerifiedrespected. Do not let a user press send for a company that is not verified yet; explain what is missing instead.- Webhook endpoint hardened. Signature verification, a fast 200, retries and idempotency on your side.
- Errors surfaced, not swallowed. Validation errors name the field that is wrong; put that in front of the person who can fix it.
- One real document, end to end. Send an invoice between two companies you control on production before letting customers in.
Volume and pricing
Documents are counted per team, with the volume of all your companies pooled,
so onboarding more companies lowers your price per document rather than adding
per-company fees. Received documents count towards the quota as well as sent
ones, so budget for both sides of the exchange. Generated XML through
generate is not billed; emails and submitted reports are.
Where to get help
- Troubleshooting. Common rejections, delivery failures and validation errors are collected in the troubleshooting guide.
- Questions. The FAQ covers Peppol, addressing, VAT and billing.
- Email. support@recommand.eu:include the company ID and, for a delivery problem, the document ID.
- Discord. Join the server for release announcements and quick questions.
- Keep track of changes. New endpoints and behaviour are recorded in the changelog.
Other guides for Germany
- Receiving Peppol documents in Germany for the companies you onboard
- Sending and receiving Peppol documents in Germany for the companies you onboard
- Sending Peppol documents in Germany for your own company
- Receiving Peppol documents in Germany for your own company
- Sending and receiving Peppol documents in Germany for your own company