Skip to content

Your first sandbox test

Create a test customer, watch it settle, and send an invoice on its behalf — free, no call, no agreement.

What this proves — and what it doesn't

Sandbox is a real integration against real registries, on the Peppol test network. It is honest about its limits, so you know what still needs proving before you go live.

What we've actually run. This walkthrough is verified end to end with the registryless 9915 test identity and the Belgian sandbox test receiver. Creation and verification are wired for every scheme in the table below; country-specific supplier sends are not something we've proven across every market with a real transmission yet. Tell us your market when you start and we'll walk the first send with you.
Sandbox provesSandbox does not prove
Your full integration — create customers over the API, receive lifecycle webhooks, send on a customer's behalfReal Peppol delivery. Documents travel the test network only
Identity verification against the real business registry — KBO/CBE, INSEE, Bolagsverket, VIES, DanskCVRAPI and Overheid.io — the same engine production usesThe customer attestation step. Sandbox never emails your customers; attestation exists only in production
The sandbox lifecycle: pending, verifying, verified, no_registry, unsupported_scheme, verification_failed, registration_failed, archivedThe attestation states. Production adds six more that never occur here — see Sub-tenant Lifecycle

Your customers are never involved and never contacted. Sandbox platform accounts are capped at 10 active test customers; archived ones no longer count.

Before you start

The sandbox trial is self-service. Every account starts in sandbox. When an organisation admin signs up and chooses A platform for my customers, the platform sandbox trial starts on the account; if you picked My own business, an organisation admin can start the trial later from the console overview. Until the trial is on, step 3 below is not available and POST /v1/legal-entities answers 403 master_key_required. Production platform access is set up with our team.
  1. 1. A console account — sign up at console.getpeppr.dev. Every account starts in sandbox.
  2. 2. The platform sandbox trial — chosen at signup, or started later by an organisation admin from the console overview. Nothing below works before that.
  3. 3. A master keyonly once the trial is on, and starting it does not create the key for you: an organisation admin opens API Keys → Create master key in the console and picks sandbox. The secret is shown once, so copy it then. A standard key returns 403 master_key_required on the endpoints below.
  4. 4. The SDK — Node.js 18+ and npm install @getpeppr/sdk (≥ 2.6.0).
  5. 5. A test identity — scheme 9915 and a value you pick yourself. The Peppol code list publishes 9915 as having no entity behind it, so there is no registry to satisfy and no real company to borrow. That is the default route, and the next section explains it. Using a real registered company instead is supported, but it must still be active at its registry.

Choosing a test identity

Verification runs against real business registries, in sandbox exactly as in production. That leaves you two honest options, and one that does not work.

An invented number is the one route that fails. A plausible-looking VAT or company number is not a test value: the registry is asked, answers that it holds no such entity, and the sub-tenant is refused for good. Re-running the check cannot change a registry's mind, so the entity has to be archived and created again. The identifier is immutable once set.

Option A — scheme 9915 (recommended)

9915 is published in the Peppol code list with the note “No entity behind id”. Nothing is registered behind it by design, so there is no registry to query and no one else's tax number involved. It is what the ecosystem uses for test participants, DG DIGIT included. The value is yours to choose: an uppercase letter followed by uppercase letters and digits.

Make the value your own — do not copy the one below verbatim. The Peppol test network is shared, and an address can only be registered once on it. If two integrators register the same value, the second one gets a sub-tenant that verifies and then never becomes send-ready. Working your account number into the value is what makes a collision unlikely — nothing can make it impossible, since the namespace is shared with everyone else on the test network. The console suggests a value on exactly that pattern when you add a test customer there.
Test customer on 9915
const customer = await peppol.legalEntities.create({
  // Same externalId used at Step 3 below.
  externalId: "customer_be_001",
  companyName: "Acme Test Ltd",
  country: "GB",
  address: { line1: "1 Test Street", city: "London", zip: "EC1A 1BB" },
  identifier: {
    scheme: "9915",
    // Uppercase letters and digits only, starting with a letter — so drop
    // the dash: account PEP-85848 becomes ACME85848TEST.
    value: "ACME85848TEST",
  },
});

// 202 straight away — verification is asynchronous.
// Once it settles: status "no_registry", and ready to send.

Verification settles on its own, usually within seconds — it runs in the background, and a five-minute sweep picks up anything that did not start. The status you will then read is no_registry, not verified: it is its own word on purpose, because nothing was proven about an entity, and calling it verified would make the sandbox promise something production will refuse. Both statuses send in sandbox.

Three countries need a real identity even in sandbox: the Netherlands, Denmark and Norway. A Dutch supplier must carry a KVK or OIN number (rule NL-R-003), a Danish one a CVR number (DK-R-002), and a Norwegian one both an organisation number and a Norwegian VAT number — and scheme 9915 is none of those. Such a sub-tenant verifies and becomes send-ready all the same, then its invoices are refused at send with a 422. For a test customer in any of the three, take Option B below.

Option B — a real registered company

Use this when what you want to exercise is registry verification itself — the name comparison, the failure modes, the timing. Pick a company that is still active: when a registry reports one as struck off, in liquidation, or not yet active, we refuse it, and re-sending the same details will not change that until the registry changes its own answer. Copy the company name from the registry entry rather than typing it from memory; it is compared against what the registry holds.

Pick a company that isn't already on Peppol through another provider. Registry verification passes either way, but the test-network registration cannot attach a second time, and the entity never becomes send-ready — so you would not be able to finish Step 3. Option A meets the same limit, which is why the value you pick there has to be your own; the difference is that you control it.

Step 1 — Create a test customer

On scheme 9915, this is the call shown in Choosing a test identity and there is nothing else to prepare. The example below takes the other route, a real Belgian company — copy the company name from the registry entry rather than typing it from memory, since it is compared against what the registry holds.

Belgian test customer
import { Peppol } from "@getpeppr/sdk";

const peppol = new Peppol({ apiKey: "sk_sandbox_your_master_key" });

const customer = await peppol.legalEntities.create({
  // Your stable reference. Echoed as data.subTenantId on every
  // Legal Entity lifecycle webhook, so you can map it straight back.
  externalId: "customer_be_001",
  companyName: "Exact Registered Legal Name",
  country: "BE",
  address: { line1: "Rue de la Loi 16", city: "Brussels", zip: "1000" },
  identifier: { scheme: "0208", value: "0685660237" },
});

console.log(customer.id, customer.status); // "7c9a1b34-…", "pending"

You get 202 immediately; verification runs in the background.

Re-sending the same externalId: an identical name and normalized identifier returns the existing record (200) without another provider create. Reusing that identifier under a different externalId returns 409 identifier_already_in_use; use the owned reference in existingLegalEntity instead. Changing the scheme or number on the original reference is refused with 422 identifier_immutable — a different identifier is a different company, so archive the entity and create it again. The externalId is free to reuse once archived.

The example is Belgian, the flow is not. For your own market, swap externalId, companyName, the legal address, country and identifier — then reuse that same externalId in sender.externalSubTenantId at Step 3. The name and address are registered as you send them, so they must match the register. The schemes we verify today, with the exact format each one accepts:

The values below are shapes, not numbers you can use. Each one shows the format a scheme accepts; none of them belongs to a company, so every one of them is refused by the registry it is checked against. Bring a real number, or take scheme 9915.
CountryschemeWhat the value is
Belgium0208KBO/CBE enterprise number — exactly 10 digits
Denmark0184CVR number — exactly 8 digits
United KingdomGB:VATGB then 9 digits (or 12 for VAT groups). The GB prefix is required — e.g. GB123456789
Ireland9935IE then 7 digits and 1-2 letters. The IE prefix is required — e.g. IE1234567T
France0002SIREN (9 digits) or SIRET (14 digits)
France0009SIRET — 14 digits only
France (CTC)0225SIREN — 9 digits only
Germany9930DE then 9 digits (USt-IdNr). The DE prefix is required — e.g. DE123456788
Netherlands0106KVK number — exactly 8 digits, including any leading zero
Sweden0007Organisationsnummer — 10 digits
Testing (not NL, DK or NO)9915A value you choose: an uppercase letter, then uppercase letters and digits. No registry stands behind this scheme, so none is queried — see Choosing a test identity

Two country specifics worth knowing before you start. Germany (9930): VIES confirms the VAT number is valid but masks the company name, so we cannot compare your declared name against the register — verification rests on the VAT number alone. United Kingdom (GB:VAT): the VAT number is the only UK identifier the Peppol network routes — a Companies House number is not a Peppol identifier and cannot receive anything. We cannot currently verify UK VAT numbers against HMRC automatically. Contact support to arrange identity verification before production sending.

A scheme we don't verify yet doesn't fail loudly — the customer simply sits at pending. If that happens, email us rather than waiting.

Step 2 — Watch it verify

Subscribe a webhook endpoint, or poll. Webhooks are the cheaper path.

Poll the status
const check = await peppol.legalEntities.get(customer.id);

// Two terminal statuses are ready to send in sandbox:
//   "verified"    — a registry confirmed the company (option B)
//   "no_registry" — scheme 9915: there was no registry to ask (option A)
const ready = check.status === "verified" || check.status === "no_registry";
There is no legal_entity.* prefix wildcard. Subscribe to the five event names explicitly — legal_entity.registered, legal_entity.unsupported_scheme, legal_entity.verification_failed, legal_entity.awaiting_authz, legal_entity.registration_failed — or use the global wildcard *, which covers the lifecycle and invoice events. A couple of specialised event types are opt-in by name only; see Webhooks.

If you poll, poll no more often than once every 10 seconds and honour Retry-After on 429: sandbox allows 10 requests per minute per key and 50 per minute per account, and this GET spends that budget.

The webhook arrives as legal_entity.registered, with data.subTenantId set to your externalId — map it straight to your customer record, no lookup needed.

Step 3 — Send on their behalf

The same POST /v1/invoices as single-tenant sending — you just add sender.

Send as your customer
await peppol.invoices.send({
  number: "INV-2026-001",
  // Required by network rule R003. Storecove has always injected one
  // when missing, but that is not contractual — set it yourself.
  buyerReference: "INV-2026-001",
  sender: { externalSubTenantId: "customer_be_001" },
  to: {
    // SPF Economie — the standard Peppol test receiver. Sandbox
    // delivers ONLY to test recipients: send to a real company and
    // the submission is accepted, then fails asynchronously.
    name: "SPF Economie",
    peppolId: "9925:BE0314595348",
    // Buyer VAT is read from this field — never derived from peppolId.
    vatNumber: "BE0314595348",
    street: "Rue du Progrès 50",
    city: "Brussels",
    postalCode: "1000",
    country: "BE",
  },
  lines: [
    // The registryless 9915 test identity has no VAT identifier, so this
    // diagnostic invoice is explicitly outside scope (O), not zero-rated (Z).
    // For a real transaction, use the category the invoice actually requires.
    { description: "Sandbox platform test", quantity: 1, unitPrice: 90, vatRate: 0, vatCategory: "O" },
  ],
});

The supplier identity — name, country, Peppol ID — is taken from the selected send-ready Legal Entity. Any from you pass is stripped, so you cannot mis-state the sender even by accident.

Use the master key for this step too. A standard key does not error here — it silently ignores sender and the invoice goes out under your own identity.

If something fails

SymptomWhat it means
403 master_key_requiredStandard key instead of a master key, or Platform mode isn't enabled yet
401 Invalid API key on a key that used to workPlatform mode was switched off — master keys are revoked with it
The invoice sends, but under your identityYou used a standard key for Step 3. sender is ignored silently for standard keys
name_mismatchCopy the company name from the registry entry and re-send the same externalId. If it still fails, send us the externalId
not_foundCheck verificationDetail.registryStatus first. If it reads inactive, the company exists but the registry doesn't consider it active (struck off, in liquidation, or not yet active) — re-sending won't change that, so use a company that is trading today. If the field is absent we have no such finding to show, which is not the same as none existing: the registry may have no entry, may have been unreachable, our team may have reviewed an earlier finding, or there may simply be no inactive finding on record — a valid VAT registration can verify a company on its own. So confirm at the national registry that the company is trading, then check for a typo and that the scheme matches the number
already_registeredRead it from registrationDetail.reason when status is registration_failed. The identifier already lives on another access point. This blocks Step 3 — pick a company that isn't registered elsewhere
invalid_format / provider_errorThese are the other stable registrationDetail.reason values. Fix the identifier format, or retry later for a provider error; raw provider text is never exposed
409 identifier_already_in_useThe same normalized participant already belongs to another active externalId in your account. Reuse the returned existingLegalEntity; no provider create ran
422 identifier_immutableYou changed the scheme or number on an existing externalId. Archive it, then recreate
422 on sendCustomer not send-ready, or a VAT-bearing invoice (including vatRate: 0) missing to.vatNumber
42910 requests per minute per key, 50 per account. Slow the polling and honour Retry-After

Stuck for more than ten minutes? Email hello@getpeppr.dev with your externalId and what you expected. You'll get the founder, not a ticket queue.

When you move to production

Same API, same SDK, same send call. What changes:

  1. 1. A production master key, which requires an active platform contract — a sandbox key never reaches the live network.
  2. 2. Attestation. Each customer confirms, on a getpeppr-signed page, that they authorise you to invoice on their behalf. You trigger it with POST /v1/legal-entities/:id/attestation; we email the co-branded link and keep the audit trail. It doesn't exist in sandbox.
  3. 3. More statuses, around that authorisation flow. Don't hard-code a fixed sequence between them — read the current status and react to it. See Sub-tenant Lifecycle.
  4. 4. Real recipients. Sandbox only delivers to the test receiver above; production reaches your customers' actual buyers.

Commercially, production platform access is set up with us directly — plan, country scope, customer cap. Email us and we'll size it with you.