DEV Community

Sayed Rashid
Sayed Rashid

Posted on

Check if a Singapore company can receive e-invoices, from one JSON endpoint

Singapore is phasing in a GST e-invoicing mandate (InvoiceNow) between 2026 and 2031. If you build or integrate billing systems here, you will eventually need to answer a very practical question:

Can this counterparty actually receive an e-invoice, and in what format?

The Peppol Directory answers it over a plain JSON endpoint. No key, no signup. Here is what I learned wiring it up, including two dead ends worth skipping.

The endpoint

https://directory.peppol.eu/search/1.0/json?country=SG&rpc=N
Enter fullscreen mode Exit fullscreen mode
curl -s "https://directory.peppol.eu/search/1.0/json?country=SG&rpc=N" | jq '."total-result-count"'
# 68205
Enter fullscreen mode Exit fullscreen mode

68,205 Singapore participants as I write this. rpc=N skips the participant-count rollup and makes the response noticeably smaller.

Paging uses resultPageIndex and resultPageCount:

curl -s "https://directory.peppol.eu/search/1.0/json?country=SG&rpc=N&resultPageIndex=0&resultPageCount=3" \
  | jq '.matches | length'
# 3
Enter fullscreen mode Exit fullscreen mode

The ID format is the part that trips people up

Singapore participants are identified as:

scheme: iso6523-actorid-upis
value:  0195:sguen<UEN>
Enter fullscreen mode Exit fullscreen mode

0195 is the Peppol issuing-agency code for Singapore's UEN, and the value is lowercase. So UEN 202319825C becomes 0195:sguen202319825c.

Get it wrong and you get a clean, silent zero results — not an error. That cost me longer than I would like to admit.

To look up one company, the two halves are joined by a double colon:

PID="iso6523-actorid-upis::0195:sguen202319825c"
curl -s -G "https://directory.peppol.eu/search/1.0/json" --data-urlencode "participant=$PID" | jq
Enter fullscreen mode Exit fullscreen mode

What comes back

{
  "total-result-count": 1,
  "matches": [
    {
      "participantID": { "scheme": "iso6523-actorid-upis", "value": "0195:sguen202319825c" },
      "entities": [
        {
          "name": [{ "name": "XIN TONGYUN LOGISTIC PTE LTD", "language": "en" }],
          "countryCode": "SG",
          "regDate": "..."
        }
      ],
      "docTypes": [ "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#conformant#urn:fdc:peppol.eu:2017:poacc:billing:international:sg:3.0::2.1" ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Two things worth noting.

name is an array of localised names, not a string. entities[0].name[0].name is the one you usually want.

docTypes is the actually useful field. It tells you what that company can receive — Invoice, CreditNote, ApplicationResponse, and which profile. If you are about to send a credit note to someone who only registered for invoices, this is where you find out first rather than after a rejected transmission.

const canReceiveCreditNote = m.docTypes.some(d => d.includes("CreditNote-2"));
Enter fullscreen mode Exit fullscreen mode

Dead end 1: you cannot call this from the browser

There is no Access-Control-Allow-Origin header on the response:

curl -sI "https://directory.peppol.eu/search/1.0/json?country=SG&rpc=N" | grep -i access-control
# (nothing)
Enter fullscreen mode Exit fullscreen mode

So fetch() from a page is blocked. You need a server-side proxy, or — if your use case is lookup rather than live sync — a periodic snapshot. For a static tool, snapshotting the SG slice and shipping it as a JSON index is far cheaper than proxying every keystroke.

Dead end 2: skip the SML DNS trick

Older Peppol material describes resolving a participant's SMP by MD5-hashing the lowercase participant ID and doing a DNS lookup against an SML zone:

B-<md5>.iso6523-actorid-upis.<sml-zone>
Enter fullscreen mode Exit fullscreen mode

I tried it against every zone still floating around in documentation and blog posts, including the old EC edelivery.tech.ec.europa.eu and the newer participant.sml.prod.tech.peppol.org. All NXDOMAIN, for a participant the Directory API resolves happily.

Unless you are building a full Access Point, the Directory API gives you what you need without touching DNS at all. Do not spend an afternoon on this like I did.

Why this matters right now

Under the IRAS GST InvoiceNow Requirement, newly GST-registered businesses come onboard from 2026, then existing registrants by turnover through 2031. Roughly 90,000 businesses end up in scope. Anyone building AR/AP tooling for the Singapore market is going to be asked "is this customer reachable yet?" — and this endpoint is the cheapest way to answer it.

I built a few free checkers on top of this data for non-technical business owners: is my accounting software InvoiceNow-ready (against the accredited-provider list), and when my business actually has to comply. Plain HTML, no signup, no analytics — the calculation runs in the browser.

If you are integrating Peppol in Singapore and hit something I missed, I would genuinely like to hear it.

Top comments (0)