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.
| Method | Path | Fungsi |
|---|---|---|
| POST | /v2/spt/inquiry-max-rev-no | Check whether an SPT already exists for a period |
| POST | /v2/spt/create-spt | Create a new SPT for a period |
| POST | /v2/spt/inquiry-spt | Read the current state of an SPT by idSpt |
| POST | /v2/spt/summary | Summary report (currently unavailable, see 8.7) |
| POST | /v2/spt/upload | Upload SPT line-item content |
| POST | /v2/spt/submit-spt | Submit the SPT to DJP for acceptance |
| POST | /v2/spt/verify-spt | Verify 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
| Method | Path | Step | Purpose |
|---|---|---|---|
| POST | /v2/spt/inquiry-max-rev-no | 1 | Check whether an SPT already exists for a period |
| POST | /v2/spt/create-spt | 2 | Create a new SPT for a period |
| POST | /v2/spt/inquiry-spt | 3 | Read the current state of an SPT by idSpt |
| POST | /v2/spt/summary | 4 | Summary report (currently unavailable, see 8.7) |
| POST | /v2/spt/upload | 5 | Upload SPT line-item content |
| POST | /v2/spt/submit-spt | 6 | Submit the SPT to DJP for acceptance |
| POST | /v2/spt/verify-spt | 7 | Verify 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.
| Concept | inquiry-max-rev-no /create-spt | inquiry-spt | summary | submit-spt | verify-spt |
|---|---|---|---|---|---|
| SPT type | JnsSpt | JnsSpt | — | jenisSpt | jnsPajak (response) |
| NPWP | Npwp | Npwp | — | npwp | npwp |
| NOP | Nop | Nop | — | nop | — |
| Tax month | MasaPajak | — | — | msPjk | msPajak (response) |
| Tax year | TahunPajak | — | — | thnPjk | thPajak (response) |
| SPT identifier | — | idSpt | spt_id | idSpt | idSpt (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.
| JnsSpt | Description | Use case |
|---|---|---|
| VAT_VATR | SPT Masa PPN (Value Added Tax — Monthly Return) | Monthly VAT return covering output and input invoices |
| ICT_WITR | SPT Masa PPh Pemotongan/Pemungutan (broader) | Monthly withholding-tax return — broader scope |
| ICT_WTR | SPT Masa PPh Withholding (narrower) | Monthly withholding-tax return — narrower scope |
| ICT_RCITR | SPT 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:
| MasaPajak | Encodes | Meaning |
|---|---|---|
| 0404 | Apr–Apr | April only |
| 0808 | Aug–Aug | August only |
| 0112 | Jan–Dec | Full year — used for annual SPT (ICT_RCITR) |
| 0103 | Jan–Mar | First 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
| Field | Type | Required | Description |
|---|---|---|---|
JnsSpt | string | Yes | SPT type (see section 8.3.3) |
MasaPajak | string | Yes | 4-character period code (see section 8.3.3) |
TahunPajak | integer | Yes | Tax year (e.g. 2026) |
Npwp | string | Yes | Taxpayer NPWP (15 or 16 digits) |
Nop | string | No | Tax-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:
| statusSpt | Meaning |
|---|---|
| DRAFT | SPT created but not yet submitted |
| CREATED | SPT has been initialised at DJP but content is not yet uploaded |
| SUBMITTED | SPT 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
| Field | Type | Required | Description |
|---|---|---|---|
JnsSpt | string | Yes | SPT type |
idSpt | string | Yes | SPT identifier returned by create-spt. Note: lowercase i, camelCase — different from the PascalCase used for sibling fields. |
Npwp | string | Yes | Taxpayer NPWP |
Nop | string | No | Tax-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:
| Field | Type | Description |
|---|---|---|
idSpt | string | SPT identifier from create-spt |
JnsSpt | string | SPT type — drives the shape of the line items |
Npwp | string | Taxpayer NPWP |
<line-items> | array | object | Line-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.
| Field | Type | Required | Description |
|---|---|---|---|
idSpt | string | Yes | SPT identifier from create-spt |
jenisSpt | string | Yes | SPT type (the JnsSpt value, but the field is named jenisSpt here) |
npwp | string | Yes | Taxpayer NPWP |
nop | string (nullable) | No | Tax-object number; null when not applicable |
msPjk | string | Yes | Tax month (MasaPajak — same 4-character encoding, but the field is named msPjk) |
thnPjk | string | Yes | Tax year as a STRING (e.g. "2025"), not an integer |
tglSpt | string | Yes | Submission date in DDMMYYYY (e.g. "18122025") |
nikPenandatangan | string | Yes | NIK of the signing person (16 digits) |
passphrase | string | Yes | Passphrase for the taxpayer's signing certificate. Transmit only over TLS; do not log. |
lampiranSpt | array | No | Optional 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
| Field | Type | Required | Description |
|---|---|---|---|
npwp | string | Yes | Taxpayer NPWP (camelCase) |
ntte | string | Yes | NTTE 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
| Field | Type | Description |
|---|---|---|
idSpt | string | SPT identifier |
noBps | string (nullable) | BPE number. Null while DJP is processing; populated when BPE is issued. Example: BPE-00001/CT/KPP.0000/2026 |
tglBps | string | BPE issue date in YYYY-MM-DD. Empty string while pending. |
msPajak | string | Tax month — note the different field name vs the request (msPjk on submit-spt, MasaPajak on inquiry) |
thPajak | string | Tax year |
jnsPajak | string | SPT type (the JnsSpt value) |
statusSpt | string | Free-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 message | Cause |
|---|---|
| SPT tidak ditemukan | No 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 message | Cause |
|---|---|
| SPT telah disampaikan dengan status: Submitted | SPT 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
| Situation | What the client sees |
|---|---|
| Successful step (any endpoint) | HTTP 200, data.status = "1", data.result carries the payload |
| DJP business-level failure | HTTP 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 registered | HTTP 403 |
| Validation error (Sipajak side) | HTTP 400 with standard NestJS error envelope (no body.response.statusMessage) |
| Server error | HTTP 500 |