The e-Faktur file format is a CSV with strict column order, encoding, and DJP validation rules — a separate engineering effort from the tax calculation itself. Building the export first surfaces these constraints early and avoids costly rework. DJP has changed the format twice in two years, so storing it as a configurable template rather than hardcoded column mappings is critical for maintainability.
DJP's system rejects the entire batch for issues like incorrect NPWP format, customer names that don't match DJP records, invoice dates outside the tax period, or duplicate NSFP numbers. The post describes a real incident where 140 e-Faktur were rejected in a single month because the NPWP validation was too lenient and accepted numbers with missing digits. Rejected submissions require manual correction and resubmission, which is painful at scale.
When paying certain service invoices — consulting, technical services, management fees, or rental — the buyer must withhold PPh 23 at 2% of the gross service fee (or 4% if the vendor has no NPWP). The AP module has a PPh 23 flag per invoice line; when checked, the payment computation automatically deducts the withholding. The system also generates a Bukti Potong PPh 23 certificate for each withholding event so the vendor can claim the credit on their own tax return.
Crediting input tax from a non-PKP supplier is a tax compliance error in Indonesia. The ERP calls DJP's NPWP validation API to confirm the number is valid and the business name matches, and checks PKP status against the DJP portal. If a vendor claims PKP status but it cannot be confirmed, the system flags the transaction before any input tax is credited.
At month-end, the tax module requires: every AR invoice with PPN to have a completed e-Faktur reference number, every AP invoice with PPh 23 to have a Bukti Potong number, the PPN ledger to balance against the e-Faktur export, and the period to be locked before the DJP filing deadline. This structured, ERP-enforced process eliminated the year-end tax reconciliation surprises the client previously encountered.
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. In my freelance ERP consulting work, I've built Indonesian tax compliance into custom ERP systems 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. The ERP I build 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 as a freelance ERP consultant: 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. I'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. The AP module in the custom ERPs I build 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). The vendor master I build 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, I 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. I 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. I encountered a batch rejection of 140 e-Faktur in a single month because my 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 the 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.