SPT

API SPT Sipajak memandu pelaporan SPT dari sistem kamu dalam tujuh langkah: cek revisi, buat SPT, baca status, upload isi, submit ke DJP, lalu verifikasi hingga Bukti Penerimaan Elektronik (BPE) terbit.

MethodPathFungsi
POST/v2/spt/inquiry-max-rev-noCheck whether an SPT already exists for a period
POST/v2/spt/create-sptCreate a new SPT for a period
POST/v2/spt/inquiry-sptRead the current state of an SPT by idSpt
POST/v2/spt/summarySummary report (currently unavailable, see 8.7)
POST/v2/spt/uploadUpload SPT line-item content
POST/v2/spt/submit-sptSubmit the SPT to DJP for acceptance
POST/v2/spt/verify-sptVerify acceptance and retrieve BPE

The SPT module orchestrates tax-return submissions to DJP. Where the other modules typically expose one resource per endpoint, SPT exposes a multi-step lifecycle: the same logical SPT progresses through inquiry, creation, content upload, submission, and verification, with seven endpoints corresponding to those steps. The endpoints are designed to be called in sequence; calling them in isolation is rarely meaningful.

SPT proxies to DJP's CTAS tax-return service. Unlike VSWP — the other DJP-passthrough module — SPT wraps DJP business errors as HTTP 400 rather than returning them inside an HTTP 200 envelope. This is the same convention used by e-Billing (section 9), and is documented in section 8.3 below.

8.1 Lifecycle overview

The diagram below shows the canonical flow. Solid arrows are sequential calls a typical integrator makes; dashed arrows are polling loops.

┌────────────────────────────────┐
│ 1. inquiry-max-rev-no          │   "Does an SPT already exist for this period?"
│    POST /v2/spt/inquiry-max-…  │   ─→ 200 with idSpt + statusSpt  (SPT exists)
└─────────┬──────────────────────┘   ─→ 400 "SPT tidak ditemukan"   (none yet)
          │
          ▼
┌────────────────────────────────┐
│ 2. create-spt                  │   Creates a new SPT for (NPWP, JnsSpt, period).
│    POST /v2/spt/create-spt     │   Returns the idSpt used by every following step.
└─────────┬──────────────────────┘
          │
          ▼
┌────────────────────────────────┐
│ 3. inquiry-spt                 │   Re-reads the SPT state from DJP at any later time.
│    POST /v2/spt/inquiry-spt    │   Useful during draft preparation and after submit.
└─────────┬──────────────────────┘
          │
          ▼
┌────────────────────────────────┐
│ 4. summary                     │   ⚠ Currently unavailable — see section 8.7.
│    POST /v2/spt/summary        │
└─────────┬──────────────────────┘
          │
          ▼
┌────────────────────────────────┐
│ 5. upload                      │   Uploads SPT line-item content (multipart).
│    POST /v2/spt/upload         │
└─────────┬──────────────────────┘
          │
          ▼
┌────────────────────────────────┐
│ 6. submit-spt                  │   Submits the SPT to DJP. Returns synchronously
│    POST /v2/spt/submit-spt     │   with an NTTE receipt-token.
└─────────┬──────────────────────┘
          │
          ▼
┌────────────────────────────────┐
│ 7. verify-spt                  │ ◀── poll until noBps is non-null
│    POST /v2/spt/verify-spt     │
└────────────────────────────────┘
 ─→ result.noBps = null      : DJP still processing — keep polling
 ─→ result.noBps populated   : BPE issued. The SPT is accepted.

Two identifiers thread through the lifecycle. The idSpt is the SPT primary key, returned by create-spt and used by steps 3–6. The NTTE (Nomor Tanda Terima Elektronik) is a receipt token returned by submit-spt and used by verify-spt to retrieve the BPE (Bukti Penerimaan Elektronik) when DJP accepts the submission.

8.2 Endpoint summary

MethodPathStepPurpose
POST/v2/spt/inquiry-max-rev-no1Check whether an SPT already exists for a period
POST/v2/spt/create-spt2Create a new SPT for a period
POST/v2/spt/inquiry-spt3Read the current state of an SPT by idSpt
POST/v2/spt/summary4Summary report (currently unavailable, see 8.7)
POST/v2/spt/upload5Upload SPT line-item content
POST/v2/spt/submit-spt6Submit the SPT to DJP for acceptance
POST/v2/spt/verify-spt7Verify acceptance and retrieve BPE

8.3 Module conventions

Four module-specific conventions apply across all SPT endpoints. Read this section before any individual endpoint reference.

8.3.1 Field naming inconsistency within the module

Important. Field naming case is not consistent across SPT endpoints. The same conceptual field appears as JnsSpt, jenisSpt, or jnsPajak depending on which endpoint receives or returns it. This is a DJP-side quirk inherited by the proxy: Sipajak forwards the keys DJP expects on each endpoint without normalising them.

The table below maps the conceptual field to the actual key on each endpoint. Match the casing exactly — DJP does not accept alternative forms.

Conceptinquiry-max-rev-no /create-sptinquiry-sptsummarysubmit-sptverify-spt
SPT typeJnsSptJnsSpt—jenisSptjnsPajak (response)
NPWPNpwpNpwp—npwpnpwp
NOPNopNop—nop—
Tax monthMasaPajak——msPjkmsPajak (response)
Tax yearTahunPajak——thnPjkthPajak (response)
SPT identifier—idSptspt_ididSptidSpt (response)

TahunPajak typing also varies: in inquiry-max-rev-no and create-spt it is a JSON integer (2026); in submit-spt thnPjk is a JSON string ("2025").

8.3.2 Soft errors return HTTP 400

Convention break. SPT does not follow the soft-error convention documented in section 3.2 — DJP business failures surface as HTTP 400, not HTTP 200 with data.status="0". The wrapping shape is identical to e-Billing (section 9.x): the original DJP envelope is preserved at body.response with the diagnostic at body.response.statusMessage. Plan error-handling code to branch on HTTP status first (200 vs 400), then read body.response for soft-error detail.

8.3.3 JnsSpt enum

The JnsSpt field identifies which kind of tax return is being filed. Four values have been observed in production traffic; refer to DJP's CTAS specification for the authoritative list.

JnsSptDescriptionUse case
VAT_VATRSPT Masa PPN (Value Added Tax — Monthly Return)Monthly VAT return covering output and input invoices
ICT_WITRSPT Masa PPh Pemotongan/Pemungutan (broader)Monthly withholding-tax return — broader scope
ICT_WTRSPT Masa PPh Withholding (narrower)Monthly withholding-tax return — narrower scope
ICT_RCITRSPT Tahunan PPh Badan (Annual Corporate Income Tax)Annual corporate income tax return

8.3.4 MasaPajak format

MasaPajak is a four-character string encoding the start and end months of the tax period as a concatenation of two two-digit month numbers (MMmm). The tax year travels separately in TahunPajak. Examples:

MasaPajakEncodesMeaning
0404Apr–AprApril only
0808Aug–AugAugust only
0112Jan–DecFull year — used for annual SPT (ICT_RCITR)
0103Jan–MarFirst quarter

For monthly returns the convention is to repeat the month (e.g. 0404 for April). For annual returns the encoding spans Jan–Dec (0112).

8.4 Inquiry maximum revision number

POST /v2/spt/inquiry-max-rev-no

Checks whether an SPT already exists for the (NPWP, JnsSpt, MasaPajak, TahunPajak) tuple. Despite the name, the endpoint does not return a numeric revision counter — it returns the current SPT's idSpt and status when one exists, or a soft error when none does. Use it as the entry point of the lifecycle to decide whether to create a fresh SPT or proceed with an existing one.

Request body

FieldTypeRequiredDescription
JnsSptstringYesSPT type (see section 8.3.3)
MasaPajakstringYes4-character period code (see section 8.3.3)
TahunPajakintegerYesTax year (e.g. 2026)
NpwpstringYesTaxpayer NPWP (15 or 16 digits)
NopstringNoTax-object number when applicable; empty string otherwise

Example request

POST /v2/spt/inquiry-max-rev-no HTTP/1.1
Host: gateway-integration.sipajak.com
Authorization: Basic <base64-encoded credentials>
npwp: 1234567890123000
Content-Type: application/json
{
  "JnsSpt": "VAT_VATR",
  "MasaPajak": "0404",
  "TahunPajak": 2026,
  "Npwp": "1234567890123000",
  "Nop": ""
}

Example response — SPT exists

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "status": "1",
    "statusCode": "200",
    "statusMessage": "Success",
    "result": {
      "idSpt": "cf587075-cddf-44cf-a893-e8eddc96d0f1",
      "statusSpt": "SUBMITTED"
    },
    "uuid": "2149eda7-8483-4dcd-8d42-deacdae2435f"
  }
}

data.result.statusSpt values observed on this endpoint:

statusSptMeaning
DRAFTSPT created but not yet submitted
CREATEDSPT has been initialised at DJP but content is not yet uploaded
SUBMITTEDSPT has been submitted to DJP

Example response — no SPT exists yet

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{
  "response": {
    "status": "0",
    "statusMessage": "SPT tidak ditemukan",
    "uuid": "9c53aaae-24a2-41d8-acb6-7f271777549f",
    "correlation_id": "882b2189-87d8-4973-a174-54860c831f5a",
    "status_code": 400
  },
  "status": 400,
  "options": {},
  "message": "Bad Request Exception",
  "name": "BadRequestException",
  "correlation_id": "882b2189-87d8-4973-a174-54860c831f5a"
}

Treat the SPT tidak ditemukan response as the signal to proceed to create-spt for this period.

8.5 Create SPT

POST /v2/spt/create-spt

Creates a new SPT at DJP for a given (NPWP, JnsSpt, MasaPajak, TahunPajak). Returns the idSpt that every subsequent endpoint references. Calling this when an SPT already exists for the period results in a soft error.

Request body

The request body shape is identical to inquiry-max-rev-no. Use the same fields.

Example request

POST /v2/spt/create-spt HTTP/1.1
Host: gateway-integration.sipajak.com
Authorization: Basic <base64-encoded credentials>
npwp: 1234567890123000
Content-Type: application/json
{
  "JnsSpt": "VAT_VATR",
  "MasaPajak": "0404",
  "TahunPajak": 2026,
  "Npwp": "1234567890123000",
  "Nop": ""
}

Example response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "status": "1",
    "statusCode": "200",
    "statusMessage": "Success",
    "result": {
      "idSpt": "a1229c1b-a509-4b29-b3d5-59b45d06020a",
      "statusSpt": "CREATED"
    },
    "uuid": "ac6281ce-7529-4ace-80e8-77d1959406c0"
  }
}

Preserve the returned idSpt — it is the handle for every subsequent step in this SPT's lifecycle.

8.6 Inquiry SPT

POST /v2/spt/inquiry-spt

Reads the current state of an SPT by idSpt. Useful between steps in the lifecycle to confirm DJP's view, and after submission to inspect the DJP internal-state text.

Request body

FieldTypeRequiredDescription
JnsSptstringYesSPT type
idSptstringYesSPT identifier returned by create-spt. Note: lowercase i, camelCase — different from the PascalCase used for sibling fields.
NpwpstringYesTaxpayer NPWP
NopstringNoTax-object number; empty string otherwise

Example response

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "status": "1",
    "statusCode": "200",
    "statusMessage": "Success",
    "result": {
      "idSpt": "a1229c1b-a509-4b29-b3d5-59b45d06020a",
      "statusSpt": "ReturnSheet Status: DRAFT. Interface History Tracking status: Completed.
",
      "jenisSpt": "VAT_VATR",
      "summaryStatus": "COMPLETED"
    },
    "uuid": "47ad935e-6437-48bf-bee6-58ca738550aa"
  }
}

statusSpt shape on this endpoint. On inquiry-spt, statusSpt is a verbose human-readable string composed of DJP's ReturnSheet Status and Interface History Tracking status fields, not the short enum returned by inquiry-max-rev-no. Parse it as free-form text — do not pattern-match on the entire string. The ReturnSheet Status prefix carries the DRAFT / CREATED / SUBMITTED state that the simpler endpoint returns.

8.7 Summary

POST /v2/spt/summary

Currently unavailable. Every observed call to /v2/spt/summary returns HTTP 400 with body.response.message = "Endpoint not found". The endpoint appears to be unimplemented on the downstream service at this time. It is reserved in the API surface for future use. Until the endpoint is enabled, clients should derive summary information by combining inquiry-spt and the e-Faktur / e-Bupot inputs that feed the SPT.

Reserved request shape

The request shape is documented here for completeness. Note that the field name on this endpoint is spt_id (snake_case), not idSpt — a deviation from sibling endpoints.

POST /v2/spt/summary HTTP/1.1
Host: gateway-integration.sipajak.com
Authorization: Basic <base64-encoded credentials>
npwp: 1234567890123000
Content-Type: application/json
{
  "spt_id": "a1229c1b-a509-4b29-b3d5-59b45d06020a"
}

8.8 Upload

POST /v2/spt/upload

Uploads the body of an SPT — the line-item content that the submission will report. The payload format depends on JnsSpt: a VAT return upload differs in shape from an annual corporate-income-tax upload. Consult DJP's CTAS specification for the format applicable to your JnsSpt.

Reference contract. Audit log captures for this endpoint are not available; the request shape below is derived from controller-level DTOs and the DJP CTAS contract. Verify against a staging probe before depending on the exact field set in production.

Common parameters present regardless of JnsSpt:

FieldTypeDescription
idSptstringSPT identifier from create-spt
JnsSptstringSPT type — drives the shape of the line items
NpwpstringTaxpayer NPWP
<line-items>array | objectLine-item content. Shape varies by JnsSpt. For VAT_VATR this typically references e-Faktur invoices already created; for ICT_WITR / ICT_WTR it references e-Bupot receipts.

8.9 Submit SPT

POST /v2/spt/submit-spt

Submits the prepared SPT to DJP. The request must be signed with the taxpayer's signing certificate; the NIK of the signatory and the certificate passphrase are part of the body.

Synchronous response, asynchronous receipt. The endpoint returns synchronously with an NTTE (Nomor Tanda Terima Elektronik). The NTTE is a receipt-token — its presence in the response means DJP has accepted the submission for processing, not that the BPE (the final receipt) has been issued. Use verify-spt to retrieve the BPE once DJP has finished processing.

Request body

All field names on this endpoint are camelCase — different from the PascalCase used in inquiry-max-rev-no and create-spt.

FieldTypeRequiredDescription
idSptstringYesSPT identifier from create-spt
jenisSptstringYesSPT type (the JnsSpt value, but the field is named jenisSpt here)
npwpstringYesTaxpayer NPWP
nopstring (nullable)NoTax-object number; null when not applicable
msPjkstringYesTax month (MasaPajak — same 4-character encoding, but the field is named msPjk)
thnPjkstringYesTax year as a STRING (e.g. "2025"), not an integer
tglSptstringYesSubmission date in DDMMYYYY (e.g. "18122025")
nikPenandatanganstringYesNIK of the signing person (16 digits)
passphrasestringYesPassphrase for the taxpayer's signing certificate. Transmit only over TLS; do not log.
lampiranSptarrayNoOptional attachments. Pass an empty array when none.

Example request

POST /v2/spt/submit-spt HTTP/1.1
Host: gateway-integration.sipajak.com
Authorization: Basic <base64-encoded credentials>
npwp: 1234567890123000
Content-Type: application/json
{
  "idSpt": "ab5493cb-2290-4a56-958f-37a6c04f210e",
  "lampiranSpt": [],
  "jenisSpt": "VAT_VATR",
  "npwp": "1234567890123000",
  "nop": null,
  "msPjk": "0202",
  "thnPjk": "2025",
  "tglSpt": "18122025",
  "nikPenandatangan": "3201234567890001",
  "passphrase": "<certificate passphrase>"
}

Example response — DJP rejection

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{
  "response": {
    "status": "0",
    "statusMessage": "SPT telah disampaikan dengan status: Submitted",
    "uuid": "8945ef9d-105c-46c9-8cc7-9e16b8a7f6d1",
    "correlation_id": "58eab123-4df7-43bd-a168-47342d55db10",
    "status_code": 400
  },
  "status": 400,
  "options": {},
  "message": "Bad Request Exception",
  "name": "BadRequestException",
  "correlation_id": "58eab123-4df7-43bd-a168-47342d55db10"
}

This rejection ("SPT telah disampaikan") is the most common error pattern observed on submit-spt — it indicates DJP already has a final-state SPT for the period.

8.10 Verify SPT

POST /v2/spt/verify-spt

Retrieves the BPE (Bukti Penerimaan Elektronik) for a previously submitted SPT. The endpoint takes the NTTE that submit-spt returned and asks DJP whether processing is complete. While DJP is still processing, the BPE number is null; once accepted, the BPE number and timestamp populate.

Polling pattern. Poll verify-spt at intervals (typically every few seconds initially, backing off to every 30 seconds) until result.noBps is non-null. There is no webhook or push notification — clients must drive the polling.

Request body

FieldTypeRequiredDescription
npwpstringYesTaxpayer NPWP (camelCase)
nttestringYesNTTE receipt-token from submit-spt

Example request

POST /v2/spt/verify-spt HTTP/1.1
Host: gateway-integration.sipajak.com
Authorization: Basic <base64-encoded credentials>
npwp: 1234567890123000
Content-Type: application/json
{
  "npwp": "1234567890123000",
  "ntte": "651f8a9a-9e5f-4231-bdbd-e5b30c39fa10"
}

Example response — BPE pending

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "status": "1",
    "statusCode": "200",
    "statusMessage": "Success",
    "result": {
      "idSpt": "8afeb77b-bf80-4d52-a809-7d87d6cc1222",
      "noBps": null,
      "tglBps": "",
      "msPajak": "12",
      "thPajak": "2025",
      "jnsPajak": "ICT_WTR",
      "statusSpt": "ReturnSheet Status: DRAFT. Interface History Tracking status:
Failed.Message: Deposit tidak mencukupi untuk menyetor atas kurang bayar SPT. "
    },
    "uuid": "246ab610-df27-49ac-939c-0d18ad94327a"
  }
}

Example response — BPE issued

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
  "status_code": 200,
  "message": "OK",
  "data": {
    "status": "1",
    "statusCode": "200",
    "statusMessage": "Success",
    "result": {
      "idSpt": "651f8a9a-9e5f-4231-bdbd-e5b30c39fa10",
      "noBps": "BPE-00001/CT/KPP.0000/2026",
      "tglBps": "2026-01-28",
      "msPajak": "10",
      "thPajak": "2025",
      "jnsPajak": "ICT_WITR"
    },
    "uuid": "d80e8fe5-9991-490a-ac71-2a0c8fa5059e"
  }
}

Response data.result fields

FieldTypeDescription
idSptstringSPT identifier
noBpsstring (nullable)BPE number. Null while DJP is processing; populated when BPE is issued. Example: BPE-00001/CT/KPP.0000/2026
tglBpsstringBPE issue date in YYYY-MM-DD. Empty string while pending.
msPajakstringTax month — note the different field name vs the request (msPjk on submit-spt, MasaPajak on inquiry)
thPajakstringTax year
jnsPajakstringSPT type (the JnsSpt value)
statusSptstringFree-form DJP status text; may carry warnings such as deposit shortfalls. Present on pending responses; may be absent on successful BPE responses.

8.11 Error catalogue

DJP returns specific Bahasa Indonesia messages on the inner body.response.statusMessage field for business-level failures. The table below lists the messages observed in staging traffic, grouped by endpoint.

inquiry-max-rev-no

DJP messageCause
SPT tidak ditemukanNo SPT exists for the (NPWP, JnsSpt, MasaPajak, TahunPajak) tuple. Expected when the integrator is about to call create-spt; not a real error.

submit-spt

DJP messageCause
SPT telah disampaikan dengan status: SubmittedSPT for this period has already been submitted. Cannot resubmit without first cancelling — refer to DJP for the cancellation procedure (not currently exposed in the Sipajak API).

8.12 Error handling reference

SituationWhat the client sees
Successful step (any endpoint)HTTP 200, data.status = "1", data.result carries the payload
DJP business-level failureHTTP 400, body.response.statusMessage carries the Bahasa Indonesia diagnostic; body.response.status = "0"
Endpoint not yet implemented (summary)HTTP 400, body.response.message = "Endpoint not found"
Authentication failure (Sipajak side)HTTP 401
NPWP header missing or not registeredHTTP 403
Validation error (Sipajak side)HTTP 400 with standard NestJS error envelope (no body.response.statusMessage)
Server errorHTTP 500