Indonesia's tax system is one of the most complex in Southeast Asia for ERP implementers. In 2025, the government finalized a 12% PPN rate applied only to luxury goods, with an effective 11% rate for most goods and services via a DPP Nilai Lain mechanism — a technical distinction that tripped up many businesses. Non-compliant taxpayers can have their e-Faktur access deactivated for failing to file PPN returns for three consecutive months, causing an immediate business disruption. At Commsult, I've built Indonesian tax compliance into our custom ERP from the ground up — covering PPN, PPh 21, PPh 23, and e-Faktur integration. This post is the practical guide I wish I had when I started.
The Indonesian taxes most relevant to an ERP system are: PPN (Pajak Pertambahan Nilai — Value Added Tax) at 11% on most goods and services; PPh 21 (Article 21 Income Tax — employee salary withholding, calculated progressively); PPh 23 (Article 23 Income Tax — withholding on services, dividends, interest at 2-15%); PPh 25 (installment corporate tax); and PPh 4(2) (final tax on certain income types like construction services at 4%). Each of these has different rates, different filing frequencies, different DJP forms, and different ERP implementation requirements.
The PPN module tracks input tax (PPN on purchases that can be credited) and output tax (PPN on sales that must be remitted). Every AR invoice to a PKP customer generates a tax invoice (e-Faktur). Every AP invoice from a PKP vendor contains input tax. The net monthly PPN liability is: output tax minus creditable input tax. Our ERP maintains a PPN ledger that aggregates this automatically from posted AR and AP invoices. The PPN is reported monthly via DJP's e-Filing portal, and the supporting e-Faktur data is uploaded via the e-Faktur desktop application or API.
Indonesian Tax Module Architecture (NestJS + PostgreSQL)
AR Invoice (PKP customer)
│ PPN 11% computed: DPP × 11%
▼ (DPP = Nilai Lain if applicable)
┌─────────────────────────────────────────────────────┐
│ e-Faktur Generation │
│ │
│ NSFP (nomor seri faktur pajak) from DJP allocation │
│ Customer NPWP (validated via DJP API) │
│ DPP (Dasar Pengenaan Pajak) │
│ PPN Amount = DPP × 11% │
│ │
│ Export → CSV (DJP format, UTF-8 BOM, 57 columns) │
└─────────────────────────────────────────────────────┘
AP Invoice (PKP vendor)
│ Input Tax: vendor's PPN
▼
┌─────────────────────────────────────────────────────┐
│ Input Tax Register │
│ Credited against Output Tax monthly │
└─────────────────────────────────────────────────────┘
Monthly PPN Closing:
Output Tax (AR e-Faktur total PPN)
− Input Tax (AP creditable PPN)
= Net PPN Payable (or Credit)
→ Filed via DJP e-Filing by last business day of next month
PPh 23 Withholding (on service invoices):
Vendor Invoice: Rp 50,000,000 (consulting fee)
PPh 23: 50,000,000 × 2% = Rp 1,000,000 (withheld by buyer)
Net Payment to vendor: Rp 49,000,000
Bukti Potong issued to vendor ← vendor uses for their own tax creditFrom my experience building ERP systems at Commsult: build the e-Faktur export first, not the PPN calculation. Many ERP implementers build the tax calculation logic but then realize the e-Faktur file format (a CSV with specific column order, encoding, and validation rules) is a completely separate engineering effort. The DJP e-Faktur format changes periodically — store the export format as a configurable template, not hardcoded column mappings. We've had to update the export format twice in two years due to DJP format changes.
When paying certain service invoices (consulting, technical services, management fees, rental), the buyer must withhold PPh 23 at 2% of the gross service fee (or 4% if the vendor doesn't have an NPWP). The withheld amount is remitted to DJP by the buyer and deducted from the vendor payment. Our AP module has a PPh23 flag on each invoice line — when checked, the payment computation automatically deducts the withholding amount. The system generates a Bukti Potong PPh 23 document (withholding certificate) for each withholding event, which the vendor needs to claim the credit on their own tax return.
Before transacting with a new vendor, Indonesian businesses should verify their NPWP (tax registration number) and PKP status (VAT-registered status). Our vendor master includes NPWP and PKP status fields. When a new vendor is created, the ERP calls DJP's NPWP validation API to verify the number is valid and the business name matches. PKP status is checked against the DJP portal. If a vendor claims to be PKP but their status doesn't confirm this, we flag it before any PPN input tax is credited — crediting input tax from a non-PKP supplier is a tax compliance error.
// NestJS: e-Faktur CSV export (DJP format)
// Reference: PER-03/PJ/2022 (DJP e-Faktur format)
@Injectable()
export class EFakturService {
// NPWP validation: exactly 15 digits, valid check digit
private validateNpwp(npwp: string): void {
const clean = npwp.replace(/[.-]/g, '');
if (!/^d{15}$/.test(clean)) {
throw new BadRequestException(
`Invalid NPWP format: ${npwp} (must be 15 digits)`
);
}
}
async generateExport(periodYear: number, periodMonth: number): Promise<Buffer> {
const invoices = await this.arRepo.findEfakturReadyInvoices(
periodYear, periodMonth
);
// DJP format: UTF-8 with BOM (Windows compatibility)
const rows = [
// Header row (DJP column specification)
'FK,KD_JENIS_TRANSAKSI,FG_PENGGANTI,...',
];
for (const inv of invoices) {
this.validateNpwp(inv.customer.npwp);
const dpp = inv.subtotal; // DPP = nilai barang/jasa (excl. PPN)
const ppn = Math.round(dpp * 0.11); // PPN 11%
rows.push([
'FK', // Kode faktur
inv.transactionType, // 01, 02, ...
inv.isReplacement ? '1' : '0', // FG pengganti
inv.nsfp, // NSFP from DJP
inv.invoiceDate.toISOString().slice(0,10).replace(/-/g,'/'),
inv.npwpSeller,
inv.customer.npwp.replace(/[.-]/g,''),
inv.customer.name.toUpperCase().slice(0, 200),
dpp.toFixed(0),
ppn.toFixed(0),
'0', // PPnBM (0 for non-luxury)
].join(','));
}
const bom = Buffer.from('', 'utf8');
const csv = Buffer.from(rows.join('
'), 'utf8');
return Buffer.concat([bom, csv]); // UTF-8 with BOM
}
}The tax module is a NestJS TaxModule with: PpnService (output/input tax tracking), PphService (21, 23, and 4(2) withholding), EFakturService (generates the export CSV), and BuktiPotongService (generates withholding certificates via Puppeteer PDF). The e-Faktur CSV generation uses a strictly validated serializer that enforces column order, encoding (UTF-8 with BOM for Windows compatibility), and all DJP-required field formats. We run the serializer against 20+ test cases including edge cases (null NPWP, non-PKP customers, corrective tax invoices) on every deployment.
e-Faktur submissions are rejected by DJP's system for data quality issues: incorrect NPWP format, customer name that doesn't match DJP records, invoice date outside the tax period, and duplicate NSFP (tax invoice serial number) usage. We encountered a batch rejection of 140 e-Faktur in a single month because our NPWP validation was too lenient and accepted NPWPs with missing digits. Build strict NPWP validation (must be exactly 15 digits, valid check digit) and customer name matching into the e-Faktur generation flow. Rejected e-Faktur require manual correction and resubmission — it's painful at scale.
At the end of each month, the tax module runs a closing checklist: all AR invoices with PPN must have a completed e-Faktur reference number (from DJP), all AP invoices with PPh23 must have a Bukti Potong number, the PPN ledger must balance (output tax on AR must equal the sum on the e-Faktur export), and the period must be locked before the DJP filing deadline (typically the last business day of the following month). This structured closing process, enforced by the ERP, eliminated the year-end tax reconciliation surprises our client used to encounter.
For annual SPT Badan (corporate income tax return) preparation, the ERP provides: annual P&L in the DJP-required format, PPh 21 annual reconciliation (total withheld must match e-SPT files), PPh 23 annual reconciliation (total withholdings match Bukti Potong issued), and PPN annual reconciliation (12 months of SPT Masa PPN match e-Faktur records). These reconciliation reports are run before the tax consultant starts preparing the SPT Badan, catching discrepancies while there's still time to correct them — not after filing.