API: Peppol lookups, watch lists and change webhooks

The E-Invoice Map API lets your own software do what the account page does: look up whether a company can receive e-invoices over Peppol and through which provider, keep watch lists, read what changed, and get changes pushed to your server. It is part of the Firm plan (plans). All answers come from the public Peppol network (the SML and each company's SMP); they are information, not an official certificate.

Authentication and limits

Create a key on your account page (owner only; it is shown once) and send it in a header:

X-API-Key: eir_…
  • Base URL: https://einvoicemap.com/api/v1. HTTPS only. Requests and answers are JSON unless noted.
  • Rate: your plan's requests per minute (Firm: 300) across all endpoints. Every answer carries X-RateLimit-Limit and X-RateLimit-Remaining on lookups; above the limit you get 429 with Retry-After.
  • The key can read and change your lists. It can never change billing, users or close the account.
  • Replacing or revoking the key on the account page stops the old one at once.

Errors

Status status field Meaning
400 / 422 invalid_request, invalid_input the input isn't valid (the error field says why)
401 not_logged_in, bad_key missing, unknown or revoked key
403 not_in_plan your plan doesn't include this
404 no_workspace, not_found no such client workspace or item
429 rate_limited too many requests; wait for Retry-After seconds
503 busy the service is busy; retry after Retry-After seconds

Look up one company

GET /lookup?id=<VAT, company or Peppol number>&cc=<country, optional>

curl -H "X-API-Key: $KEY" "https://einvoicemap.com/api/v1/lookup?id=BE0403170701"
{
  "status": "ok",
  "countries": ["BE"],
  "candidates": ["0208:0403170701", "9925:be0403170701"],
  "match": {
    "pid": "0208:0403170701",
    "status": "ok",
    "access_point": {"organisation": "Basware Corporation", "seat_id": "PFI000010", "country": "FI"},
    "smp_host": "smp.tickstar.com",
    "receives_invoices": true,
    "document_types": ["Peppol BIS Billing 3.0 invoice", "Peppol BIS Billing 3.0 credit note"],
    "transport": "peppol-transport-as4-v2_0",
    "certificate": {"issuer": "PEPPOL ACCESS POINT CA - G3", "issuer_ok": true, "not_after": "2028-02-28",
                    "sha256": "fcdf3ad4…"}
  },
  "other_matches": [],
  "checked_at": "2026-10-09T19:59:09+00:00",
  "cached": false,
  "products": [{"product": "…", "provider": "Basware"}]
}

status is ok (registered), no_naptr (not on Peppol), smp_404 / sg_empty (registration broken), invalid_input, or timeout / error (try again). receives_invoices: false means registered but no invoice type listed. Answers are cached for 24 hours (cached: true).

Watch lists

Your lists are the same as on the account page. kind is own (your own company's IDs) or customer; workspace is a client workspace id (0 or omitted = your main list).

Method and path What
GET /watch?kind=customer&workspace=0&limit=500&offset=0 the list with each ID's current state, totals, your plan's limit
POST /watch add {"kind": "customer", "workspace": 0, "rows": [{"id": "BE0403170701", "cc": "BE", "ref": "C-17"}]} (up to 5,000 rows)
POST /watch/remove remove {"ids": [<entry id>, …]}
GET /watch/events?days=30&workspace=0 what changed (see the kinds below)
GET /watch/export.csv?kind=customer&workspace=0 the list as CSV
GET /watch/history.csv?workspace=0 every change in your plan's history window, as CSV
GET /watch/evidence.pdf?workspace=0 the evidence record (PDF)
GET /watch/onboarding.csv?workspace=0&lang=en the "please join Peppol" letter for every customer who can't receive yet
GET /workspaces · POST /workspaces {"name"} client workspaces
GET /workspaces/<id>/report.pdf?month=2026-09 a client's readiness report

Adding answers with counts: {"added": 1, "duplicates": 0, "over_limit": 0, "invalid_count": 0, "used": 12, "limit": 50000}. New IDs are checked within about 15 minutes, then daily.

curl -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
     -d '{"kind":"customer","rows":[{"id":"BE0403170701","ref":"C-17"}]}' "https://einvoicemap.com/api/v1/watch"

Change kinds: first_check, became_reachable, no_longer_reachable, provider_changed, formats_changed, registered_twice, registered_twice_resolved, certificate_expiring (within 30 days), certificate_expired, certificate_renewed.

Webhook

Set an https URL on the account page (public host, port 443). After each check run (every 15 minutes) we POST your new changes there:

{"type": "watch.changes", "sent_at": "2026-10-10T08:15:03+00:00", "events": [
  {"id": 812, "at": "2026-10-10T08:14:51+00:00", "kind": "provider_changed",
   "detail": {"from": "Billit nv", "from_seat": "PBE000076", "to": "Basware Corporation", "to_seat": "PFI000010"},
   "list": "customer", "input": "BE 0403.170.701", "ref": "C-17", "workspace": 0, "client": ""}]}

Answer with any 2xx status. Anything else, or no answer within 10 seconds, and we send the same changes again after the next run. At most 500 events per request; more follow in the next one.

Check the signature before trusting a request. The header is X-EIM-Signature: t=<unix time>,v1=<hex>, where v1 is the HMAC-SHA256 of <t>.<raw body> with your webhook secret (shown once when you save the URL):

import hashlib, hmac, time

def valid(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    want = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts["v1"])
const crypto = require("crypto");
function valid(rawBody, header, secret) {
  const p = Object.fromEntries(header.split(",").map(x => x.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const want = crypto.createHmac("sha256", secret).update(p.t + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(p.v1));
}

Stability

This is version 1 (/api/v1). We add fields without notice; we don't remove or rename fields or change their meaning within v1. Breaking changes come as /api/v2, announced by email at least 90 days ahead.

Need help integrating? If your software sends e-invoices and you are stuck, see Integration help.

Last updated: