e-Faktur Integration for Indonesian ERP: PPN Done Right

Photo by kozumel on flickr
e-Faktur is the electronic tax invoice framework Indonesia's Directorate General of Taxes, or DJP, requires for any PKP, a registered taxable entrepreneur, selling goods or services. If your ERP issues sales invoices for an Indonesian entity that is PKP-registered, it needs a module that produces DJP-compliant invoice numbers and tax data, either through the legacy e-Faktur desktop client or the newer Coretax platform that replaced it.
Calculate the Dasar Pengenaan Pajak, the taxable base, after applying line-level discounts, then apply the 11 percent effective rate to that base, and round per invoice line rather than per document total. Store the rate as versioned configuration tied to an effective date, since Indonesian VAT rates have changed before and can change again without a code deployment if you build it that way.
Indonesia moved to a unified 16 digit taxpayer identification format so resident individuals could use their existing 16 digit NIK, the national identity number on the KTP card, directly as their NPWP starting January 2024. The old 15 digit dashed format was fully retired from DJP administrative services in mid 2024, so any system still validating that shorter format will accept taxpayer ids that DJP no longer recognizes.
NSFP, Nomor Seri Faktur Pajak, is the fiscal invoice serial number, and it cannot be freely generated by your own ERP. Under the legacy e-Faktur system it was requested in blocks through the e-Nofa application, while under Coretax, the platform that replaced e-Faktur at the end of 2025, it is issued automatically by DJP's system the moment an invoice is submitted and digitally signed.
Build the export from a dedicated read model captured at invoice-finalization time, not from live transactional tables that could still change, and validate every taxpayer id, date, and numeric field against the target schema before submission. Persist the exact payload you sent alongside the invoice record, and log DJP's response, including the assigned NSFP, back onto that record so accounting has one place to check status.

Photo by kozumel on flickr
Every taxable entrepreneur, known as PKP, that sells goods or services in Indonesia has to issue a Faktur Pajak, the legal tax invoice recognized by the Directorate General of Taxes, or DJP. If you are building or extending an ERP for an Indonesian business, the tax invoice module is not optional polish, it is the piece that keeps the whole system compliant, because a badly formed invoice number or a miscalculated PPN line can trigger penalties during an audit.
This post walks through the four pieces that actually matter when you wire tax invoicing into a custom ERP: validating the taxpayer identification number, calculating PPN correctly on the taxable base, generating invoice numbers that DJP will accept, and producing an export file the tax system can ingest. The underlying government system moved from the old e-Faktur desktop client to the unified Coretax platform, and the article covers both, because plenty of ERPs still support legacy data alongside the new flow.
NPWP, short for Nomor Pokok Wajib Pajak, is the taxpayer identification number DJP assigns to every registered business and individual. Since January 2024, resident individual taxpayers use their 16 digit NIK, the national identity number printed on the KTP card, as their NPWP directly, while companies, government bodies, and non-resident taxpayers keep a separate 16 digit NPWP. The older 15 digit format, written as 00.000.000.0-000.000, was fully retired from DJP administrative services in mid 2024, so any ERP that still stores or validates that dashed format will silently accept invalid customer records.
Run a nightly reconciliation job that flags any customer or vendor master record whose taxpayer id is not exactly 16 numeric digits. Catching this before an invoice is created is far cheaper than fixing a rejected submission after the fact.
PPN, Indonesia's value added tax, is calculated on the Dasar Pengenaan Pajak, or DPP, which is the taxable base, not always the full invoice subtotal. April 2022 raised the rate from 10 percent to 11 percent for most goods and services, and a further regulation set the statutory rate at 12 percent starting in 2025 while simultaneously introducing an adjusted tax base so the effective amount collected on standard transactions still lands at 11 percent. That distinction between the statutory rate printed in the law and the effective rate applied to the DPP is exactly the kind of detail a tax engine has to encode explicitly, because a naive implementation that multiplies the subtotal by whatever the current headline rate is will overcharge every customer the moment the statutory number changes again. Your ERP's tax engine needs to compute the DPP first, applying any discounts, then apply the effective rate, then round per invoice line, because rounding at the invoice total level instead of the line level is a common cause of centime-level mismatches during DJP validation.
// Normalize a taxpayer id before it ever reaches an invoice row.
// Since Jan 2024, resident individuals use their 16-digit NIK as NPWP;
// companies and non-residents keep a separate 16-digit NPWP.
function normalizeTaxId(raw: string): { digits: string; valid: boolean } {
const digits = raw.replace(/\D/g, "")
// 15-digit legacy format was retired; only 16-digit format is accepted now
const valid = digits.length === 16
return { digits, valid }
}
// PPN calculation on the Dasar Pengenaan Pajak (DPP), rounded per line
function calcPpn(dppRupiah: number, ratePercent = 11): {
dpp: number
ppn: number
total: number
} {
const dpp = Math.round(dppRupiah)
const ppn = Math.round(dpp * (ratePercent / 100))
return { dpp, ppn, total: dpp + ppn }
}It's tempting to hardcode PPN as 0.11 somewhere deep in a pricing service and move on. The problem shows up the moment finance needs to reprocess an old invoice, run a what-if simulation for a future rate change, or handle a transitional period where old and new rates coexist for goods shipped versus invoiced on different dates. Treat the rate as versioned reference data with an effective_from date, resolved at the moment the invoice DPP is calculated, not at the moment the sales order was created.
Never calculate PPN from a customer-facing display price that already includes rounding artifacts from currency formatting. Always calculate off the underlying numeric DPP stored at full precision, then round only for display and for the final tax line, or you will accumulate drift across thousands of invoices.
NSFP, Nomor Seri Faktur Pajak, is the fiscal invoice serial number, and it is the one part of the invoice your ERP does not get to invent freely. Under the legacy e-Faktur system, a PKP requested blocks of serial numbers through the e-Nofa application and then assigned them sequentially inside the desktop client, which meant your ERP had to track a local pool of pre-allocated numbers and warn finance before the pool ran dry. Under Coretax, the platform that formally replaced e-Faktur at the end of 2025, that manual allocation step disappears entirely: the NSFP is generated automatically the moment an invoice is submitted and digitally signed, and the format expanded from 16 to 17 digits, with older legacy-format numbers getting a 9 inserted automatically at the fifth digit when that data flows into Coretax. If your ERP still writes invoices through the desktop client for a transitional period, expect the number your system displays locally to differ from the number Coretax ultimately records, and reconcile against the platform's version, not your own.
| Field | Legacy e-Faktur | Coretax |
|---|---|---|
| Serial length | 16 digits | 17 digits |
| Allocation method | Requested in blocks via e-Nofa | Auto-issued on submission and signature |
| Submission format | Client-side XML upload | Real-time API/portal validation |
| Correction path | Manual replacement invoice | In-platform revision or cancellation |
Regardless of which generation of the system you target, the export is structured data describing header fields, taxpayer identities on both sides of the transaction, line items with their own DPP and PPN, and a checksum or signature block. Treat the export as a strict contract, not a best-effort dump, because a single malformed field rejects the whole batch rather than just one invoice.
Separating invoice finalization from invoice submission, with an explicit status field in between, is the single change that most reduces tax-invoice incidents in production ERPs. It gives finance a checkpoint to catch mistakes before anything reaches DJP.
A few final things worth verifying before this module goes live for a real PKP business, since fixing any of these after the first production invoice batch is considerably more painful than catching them in code review.