Skip to main content
BlogERP

Indonesian Tax Compliance in a Custom ERP: PPN, PPh, and e-Faktur Integration

A complete guide to implementing Indonesian tax compliance in a custom ERP — PPN (VAT) at 12% on DPP Nilai Lain, PPh withholding, e-Faktur integration, and SPT reporting automation.

Matthews Wong

Software Engineer

June 202512 min read
ERP

Key Takeaway

Indonesian tax handling is among the hardest parts of an ERP build. Since 2025 PPN is 12 percent, charged on the full price only for luxury goods and on an 11/12 base for everything else, an effective 11 percent, and the ERP has to encode that alongside PPh withholding and e-Faktur submission.

Indonesia's tax system is one of the most complex in Southeast Asia for ERP implementers. In 2025, the PPN rate became 12%, charged on the full price only for luxury goods subject to PPnBM and on a DPP Nilai Lain of 11/12 of the price for most goods and services, an effective 11% — a technical distinction that tripped up many businesses. Under PER-19/PJ/2025, a PKP that fails to file its PPN returns for three consecutive months can have its access to create tax invoices (e-Faktur) deactivated, 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.

Understanding Indonesian Tax in the ERP Context

The Indonesian taxes most relevant to an ERP system are: PPN (Pajak Pertambahan Nilai — Value Added Tax) at 12% since 1 January 2025, charged on a DPP Nilai Lain of 11/12 of the price for most goods and services (an effective 11%) and on the full price for luxury goods subject to PPnBM; 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 such as construction services, where PP 9/2022 sets seven rates from 1.75% to 6% depending on the type of work and whether the provider holds a certificate). Each of these has different rates, different filing frequencies, different DJP forms, and different ERP implementation requirements.

The PPN (VAT) Module

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 in the SPT Masa PPN, which since Coretax went live on 1 January 2025 is filed in Coretax DJP; the e-Faktur data is created in the Coretax portal, uploaded as XML built to DJP's templates, or sent through a PJAP integration, and since 12 February 2025 normal invoices may also come from the e-Faktur desktop client.

Indonesian Tax Module Architecture (NestJS + PostgreSQL)

  AR Invoice (PKP customer)
       │  PPN 12% on DPP Nilai Lain (11/12 × DPP)
       ▼  (full DPP for goods subject to PPnBM)
  ┌─────────────────────────────────────────────────────┐
  │  e-Faktur Generation                                │
  │                                                     │
  │  NSFP assigned by DJP when the e-Faktur is approved │
  │  Customer NPWP/NIK (16 digits) + NITKU (22 digits)  │
  │  DPP (Dasar Pengenaan Pajak) + DPP Nilai Lain       │
  │  PPN = 12% × 11/12 × DPP (non-luxury goods)         │
  │                                                     │
  │  Export → XML (DJP Coretax template)                │
  └─────────────────────────────────────────────────────┘

  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)
  → SPT Masa PPN filed in Coretax 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 credit

From 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 import file (a CSV with a fixed column order for the legacy desktop client, and XML built to DJP's templates since Coretax) is a completely separate engineering effort. The DJP e-Faktur format changes periodically — store the export format as a configurable template, not hardcoded field mappings. I've had to update the export format twice in two years due to DJP format changes.

PPh 23 Withholding on Vendor Invoices

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.

NPWP Validation and PKP Status Checking

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 XML export for Coretax DJP (live since 1 Jan 2025)
// Element names follow DJP's sample template (Faktur PK v1.4). DJP revises
// its templates, so pin the version your serializer targets.

@Injectable()
export class EFakturService {
  // NPWP: exactly 16 digits (a resident individual may use the 16-digit NIK).
  // NITKU (IDTKU in the XML): 22 digits, the 16-digit NPWP + a 6-digit sequence.
  private validateTaxId(value: string, digits: 16 | 22, label: string): string {
    const clean = value.replace(/\D/g, '');
    if (clean.length !== digits) {
      throw new BadRequestException(
        `Invalid ${label}: ${value} (must be ${digits} digits)`
      );
    }
    return clean;
  }

  async generateExport(periodYear: number, periodMonth: number): Promise<string> {
    const invoices = await this.arRepo.findEfakturReadyInvoices(
      periodYear, periodMonth
    );
    const sellerTin = this.validateTaxId(this.company.npwp, 16, 'NPWP');

    const taxInvoices = invoices.map((inv) => {
      const buyerTin = this.validateTaxId(inv.customer.npwp, 16, 'NPWP/NIK');
      const buyerIdtku = this.validateTaxId(inv.customer.nitku, 22, 'NITKU');

      const goods = inv.lines.map((line) => {
        // PMK 131/2024: PPN 12% on DPP Nilai Lain (11/12) for non-luxury goods;
        // PPnBM goods use the full DPP, so OtherTaxBase equals TaxBase.
        // PER-11/PJ/2025 Art. 129: whole rupiah, 0.50 and above rounds up.
        const taxBase = line.netAmount;  // after line discount, excl. PPN
        const otherTaxBase = line.isLuxury
          ? taxBase
          : Math.round((taxBase * 11) / 12);
        const vat = Math.round((otherTaxBase * 12) / 100);
        return [
          '<GoodService>',
          `<Name>${escapeXml(line.name)}</Name>`,  // your XML-escaping helper
          `<TaxBase>${taxBase}</TaxBase>`,
          `<OtherTaxBase>${otherTaxBase}</OtherTaxBase>`,
          '<VATRate>12</VATRate>',
          `<VAT>${vat}</VAT>`,
          // ...Opt, Code, Unit, Price, Qty, TotalDiscount, STLGRate, STLG
          '</GoodService>',
        ].join('');
      });

      return [
        '<TaxInvoice>',
        `<TaxInvoiceDate>${inv.invoiceDate.toISOString().slice(0, 10)}</TaxInvoiceDate>`,
        `<TrxCode>${inv.transactionType}</TrxCode>`,  // e.g. 04 = DPP Nilai Lain
        `<BuyerTin>${buyerTin}</BuyerTin>`,
        `<BuyerIDTKU>${buyerIdtku}</BuyerIDTKU>`,
        // ...remaining header elements per DJP's template
        `<ListOfGoodService>${goods.join('')}</ListOfGoodService>`,
        '</TaxInvoice>',
      ].join('');
    });

    return [
      '<?xml version="1.0" encoding="utf-8"?>',
      '<TaxInvoiceBulk>',
      `<TIN>${sellerTin}</TIN>`,
      `<ListOfTaxInvoice>${taxInvoices.join('')}</ListOfTaxInvoice>`,
      '</TaxInvoiceBulk>',
    ].join('\n');
  }
}

How I Approached This

The tax module is a NestJS TaxModule with: PpnService (output/input tax tracking), PphService (21, 23, and 4(2) withholding), EFakturService (generates the e-Faktur import file; it was a CSV for the legacy desktop client, and Coretax, live since 1 January 2025, takes XML built to DJP's templates instead), and BuktiPotongService (generates withholding certificates via Puppeteer PDF). The e-Faktur generation uses a strictly validated serializer that enforces field order, encoding (for the CSV, 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 uploads fail DJP's validation when the data is wrong: DJP's XML templates check values such as the NPWP and NITKU, so an ID in the wrong format is rejected. I once had 140 e-Faktur rejected in a single month because my NPWP validation was too lenient and accepted NPWPs with missing digits. Build strict NPWP validation (exactly 16 digits, digits only, for the NPWP or the buyer's NIK) into the e-Faktur generation flow, and validate every file against DJP's template before you upload it. Rejected e-Faktur require manual correction and resubmission — it's painful at scale.

Monthly Tax Closing Process

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 SPT Masa PPN deadline (the end of the following month, or the next working day when that falls on a weekend or holiday, under PMK 81/2024). This structured closing process, enforced by the ERP, eliminated the year-end tax reconciliation surprises the client used to encounter.

Annual Tax Reporting and Reconciliation

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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

Why should ERP implementers build the e-Faktur export before the PPN calculation logic?

The e-Faktur import file is a separate engineering effort from the tax calculation itself: a CSV with strict column order and encoding for the legacy desktop client, and since Coretax went live on 1 January 2025, XML built to DJP's templates, which validate NPWP, NITKU and other fields. 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 field mappings is critical for maintainability.

What happens if e-Faktur submissions contain data quality errors?

DJP's system rejects e-Faktur submissions with 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.

How does PPh 23 withholding work in the AP module?

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.

Why does the ERP validate a vendor's NPWP and PKP status before crediting PPN input tax?

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.

What does the monthly tax closing checklist enforce, and what problem does it solve?

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.