Before sending an e-invoice via Peppol, you need to know your customer’s Peppol ID and verify they can receive invoices. The e-invoice.be API provides two distinct lookup methods, each with different data sources and use cases:
/api/lookup - Direct SMP lookup (real-time, always accurate)
/api/lookup/participants - Peppol Directory search (broader search, may be incomplete)
Understanding the difference between these endpoints is crucial for reliable participant verification.
A Peppol ID is a unique identifier for organizations registered on the Peppol network. It consists of:
Scheme: The identifier scheme (e.g., 0208 for Belgian companies)
Identifier: The actual ID
Format: scheme:identifier → 0208:0123456789
Belgian Peppol IDs use the CBE number (Crossroads Bank for Enterprises number), which is equivalent to the Belgian VAT number without the ‘BE’ prefix. For example, if the VAT number is BE0123456789, the Peppol ID is 0208:0123456789 (numbers only).
Organizations must be registered with a Peppol Access Point to receive e-invoices. If a customer is not registered, they cannot receive invoices via Peppol.
Performs an exact, real-time lookup directly against the Service Metadata Publisher (SMP) using a specific identifier. This queries the authoritative source for participant registration information.
Searches the official Peppol Directory database, which contains participant information that access points have voluntarily published. This is a proxy for the public Peppol Directory.
Not all registered Peppol participants appear in the Directory. Publishing to the Peppol Directory is optional, not mandatory. An access point may choose not to synchronize participant data with the directory, even though those participants are fully registered and can receive invoices.
Combine both endpoints for the best user experience:
Copy
async function findAndValidateCustomer(companyName, cbeNumber = null) { // Step 1: If we have exact CBE number, use direct lookup if (cbeNumber) { console.log(`Looking up CBE ${cbeNumber} directly...`); try { const peppolId = `0208:${cbeNumber}`; const response = await api.get('/api/lookup', { params: { peppol_id: peppolId } }); if (response.data.registered) { console.log('✓ Found via direct SMP lookup'); console.log(' Name:', response.data.name); return response.data; } } catch (error) { if (error.response?.status === 404) { console.log('✗ Not registered on Peppol network'); return null; } console.error('Direct lookup failed:', error.message); return null; } } // Step 2: Search directory by name console.log(`Searching directory for "${companyName}"...`); try { const searchResponse = await api.get('/api/lookup/participants', { params: { query: companyName, country_code: 'BE' } }); if (!searchResponse.data.results || searchResponse.data.results.length === 0) { console.log('⚠ No results in Peppol Directory'); console.log(' Note: This does not mean the company is unregistered.'); console.log(' Some access points do not publish to the directory.'); console.log(' If you have the CBE number, use direct lookup instead.'); return null; } console.log(`Found ${searchResponse.data.results.length} participant(s) in directory:`); searchResponse.data.results.forEach((p, i) => { const peppolId = `${p.identifiers[0].scheme}:${p.identifiers[0].value}`; console.log(` ${i + 1}. ${p.name} (${peppolId})`); }); // Step 3: Validate the first result via direct lookup const firstResult = searchResponse.data.results[0]; const firstPeppolId = `${firstResult.identifiers[0].scheme}:${firstResult.identifiers[0].value}`; console.log(`\nValidating ${firstResult.name} via direct SMP lookup...`); const validateResponse = await api.get('/api/lookup', { params: { peppol_id: firstPeppolId } }); if (validateResponse.data.registered) { console.log('✓ Confirmed registration via SMP'); return validateResponse.data; } } catch (error) { console.error('Search failed:', error.message); return null; }}// Example usage// With exact CBE number (preferred)await findAndValidateCustomer('E-INVOICE', '1018265814');// With company name only (discovery)await findAndValidateCustomer('E-INVOICE');
async function sendInvoiceToCustomer(invoiceData) { const peppolId = invoiceData.customer.party_legal_entity.company_id; try { // Check if customer is on Peppol const validation = await api.get('/api/lookup', { params: { peppol_id: peppolId } }); if (validation.data.registered) { // Send via Peppol return await sendViaPeppol(invoiceData); } } catch (error) { // Fallback to email with PDF console.log('Customer not on Peppol, sending PDF via email'); return await sendPdfViaEmail(invoiceData); }}
Before sending invoices, use /api/lookup with the exact Peppol ID:
Copy
// ✓ Correct: Direct lookup for validationconst peppolId = `0208:${cbeNumber}`;try { const validation = await api.get('/api/lookup', { params: { peppol_id: peppolId } }); if (validation.data.registered) { await createAndSendInvoice(invoiceData); }} catch (error) { // Participant not found console.log('Customer not registered on Peppol');}// ✗ Wrong: Using directory search for validationconst search = await api.get('/api/lookup/participants', { params: { query: cbeNumber }});// May return no results even if registered!
Use Directory Search for Discovery Only
The Peppol Directory is excellent for discovery, not validation:
Copy
// Use case: User types company name in autocompleteasync function autocompleteSearch(userInput) { const results = await api.get('/api/lookup/participants', { params: { query: userInput, country_code: 'BE' } }); // Display results as suggestions return results.data.results || [];}// When user selects a result, validate via direct lookupasync function onCustomerSelected(identifier) { const peppolId = `${identifier.scheme}:${identifier.value}`; try { const participant = await api.get('/api/lookup', { params: { peppol_id: peppolId } }); return participant.data; } catch (error) { return null; }}
Different countries use different schemes. Build a mapping:
Copy
const PEPPOL_SCHEMES = { 'BE': '0208', // Belgian CBE (numbers only, no 'BE' prefix) 'NL': '0106', // Dutch KVK 'DE': '0204', // German VAT 'FR': '0009', // French SIRET 'UK': '0088', // UK GLN};function buildPeppolId(countryCode, identifier) { const scheme = PEPPOL_SCHEMES[countryCode]; if (!scheme) { throw new Error(`Unknown country: ${countryCode}`); } return `${scheme}:${identifier}`;}
Provide Clear User Feedback
Explain why directory search may not find participants:
Copy
const searchResults = await api.get('/api/lookup/participants', { params: { query: companyName }});if (!searchResults.data.results || searchResults.data.results.length === 0) { console.log(` No results found in Peppol Directory for "${companyName}". This does not necessarily mean they are unregistered. Some Peppol access points do not publish participant data to the directory, even though those participants can receive invoices. If you have the customer's CBE number or Peppol ID, use that for a direct lookup instead. `);}
Handle Missing Directory Entries Gracefully
Don’t assume a participant is unregistered if not in directory:
Copy
async function lookupCustomer(cbeNumber) { const peppolId = `0208:${cbeNumber}`; try { // Try direct lookup const response = await api.get('/api/lookup', { params: { peppol_id: peppolId } }); if (response.data.registered) { return { found: true, data: response.data, source: 'SMP' }; } } catch (error) { // Not registered or error return { found: false, message: 'Customer not registered on Peppol network', suggestion: 'Ask customer to register at https://app.e-invoice.be' }; }}
The participant is fully registered and can receive invoices, but does not appear in directory searches. This demonstrates why /api/lookup must be used for validation before sending invoices.