Skip to content

Your first sandbox test

Create a test customer, watch it verify against a real business registry, 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 from Belgium. Creation and verification are wired for every scheme in the table below; the send step from the other countries is not something we've proven 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 and VIES — the same engine production usesThe customer attestation step. Sandbox never emails your customers; attestation exists only in production
The sandbox lifecycle: pending, verifying, verified, 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

Start by emailing us — you cannot switch platform mode on yourself. Every account starts in sandbox, but the platform endpoints stay closed until we enable platform mode on your account. Write to hello@getpeppr.dev with your account name and we turn it on the same working day. Until then, step 3 below is not available to you and POST /v1/legal-entities answers 403 master_key_required.
  1. 1. A console account — sign up at console.getpeppr.dev. Every account starts in sandbox.
  2. 2. Platform mode — ask us to enable it. This is the one step that is not self-serve. Email us and we switch it on for your sandbox account the same working day. Nothing below works before that.
  3. 3. A master keyonly once we've enabled platform mode, and enabling 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 company — a real registered company that is still active at its registry, because verification runs against the real registry even in sandbox. Fake numbers will (correctly) fail, and so will a company that has been dissolved, is in liquidation, or is not yet active — the registry answers, but it answers that the company isn't trading.
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.

Step 1 — Create a test customer

Copy the company name from the registry entry rather than typing it from memory — 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 identifier returns the existing record (200). Changing the scheme or number 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:

CountryschemeWhat the value is
Belgium0208KBO/CBE enterprise number — exactly 10 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. DE123456789
Sweden0007Organisationsnummer — 10 digits

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 do not verify UK VAT numbers against HMRC automatically yet, so a UK registration is reviewed by hand before it can send.Tell us and we'll clear it.

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);
console.log(check.status); // "verified" once it is ready to send
There is no legal_entity.* prefix wildcard. Subscribe to the four event names explicitly — legal_entity.registered, 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: [
    // vatCategory is spelled out on purpose: a 0 rate without it maps
    // to "Z" (zero-rated) anyway, so make the intent explicit rather
    // than rely on the default. Not tax advice — use the category your
    // own invoice actually calls for.
    { description: "Consultation", quantity: 1, unitPrice: 90, vatRate: 0, vatCategory: "Z" },
  ],
});

The supplier identity — name, country, Peppol ID — is taken from the customer's verified 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_registeredThe identifier already lives on another access point. This blocks Step 3 — pick a company that isn't registered elsewhere
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.