Authentication
Balaka uses OAuth 2.0 Device Authorization Flow (RFC 8628) for API authentication. This flow is designed for devices and CLI tools that cannot open a browser-based login form directly.
Authentication Flow
CLI/Device Balaka API Browser
| | |
|-- POST /api/device/code --->| |
|<-- deviceCode, userCode ----| |
| | |
| (display userCode to user) |
| | |
| |<-- user visits /device ----->|
| | enters userCode |
| | clicks Authorize |
| | |
|-- POST /api/device/token -->| |
|<-- accessToken, Bearer -----| |
| | |
|-- GET /api/analysis/... -->| |
| Authorization: Bearer XXX | |
Step 1: Request Device Code
POST /api/device/code
Content-Type: application/json
{
"clientId": "my-integration"
}
Response:
{
"deviceCode": "abc123...",
"userCode": "ABCD-EFGH",
"verificationUri": "https://balaka.example.com/device",
"verificationUriComplete": "https://balaka.example.com/device?code=ABCD-EFGH",
"expiresIn": 900,
"interval": 5
}
deviceCode-- opaque string used to poll for the token (do not show to user).userCode-- short code the user enters in their browser.verificationUri-- URL where the user authorizes the device.expiresIn-- device code validity in seconds (15 minutes).interval-- minimum polling interval in seconds.
Step 2: User Authorization
The user opens verificationUriComplete in their browser (or navigates to verificationUri and enters the userCode manually). They must be logged in to Balaka. After entering the code, they click "Authorize" to grant access.
Step 3: Poll for Token
While the user authorizes, the client polls for an access token:
POST /api/device/token
Content-Type: application/json
{
"deviceCode": "abc123..."
}
Pending response (HTTP 400):
{
"error": "authorization_pending",
"errorDescription": "The authorization request is still pending"
}
Success response (HTTP 200):
{
"accessToken": "bk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"tokenType": "Bearer",
"expiresIn": 2592000,
"scope": "drafts:create,drafts:approve,drafts:read,analysis:read,analysis:write,transactions:post,data:import,bills:read,bills:create,bills:approve,tax-export:read,accounts:read,accounts:write"
}
Error responses (HTTP 400):
| error | meaning |
|---|---|
authorization_pending | User has not yet authorized. Keep polling. |
expired_token | Device code expired (15 min). Restart from Step 1. |
access_denied | User denied authorization. |
invalid_request | Invalid or missing device code. |
Step 4: Use the Token
Include the token in the Authorization header for all API requests:
GET /api/analysis/trial-balance?asOfDate=2025-12-31
Authorization: Bearer bk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Scopes
Tokens are issued with all available scopes. Each scope maps to a SCOPE_ authority in Spring Security:
| Scope | Grants access to |
|---|---|
drafts:create | Create draft transactions |
drafts:approve | Approve/reject drafts |
drafts:read | Read draft transactions |
analysis:read | Financial reports, snapshots, trial balance, balance sheet, income statement, cash flow |
analysis:write | Publish AI analysis reports |
transactions:post | Create, update, post, void, delete transactions; free-form journal entries |
data:import | Import industry seed data |
bills:read | Read vendor bills |
bills:create | Create vendor bills |
bills:approve | Approve vendor bills |
tax-export:read | Tax reports, SPT data, payroll, employees, fiscal adjustments, salary components |
accounts:read | Read chart of accounts |
accounts:write | Create/update/delete chart of accounts |
Token Properties
- Validity: 30 days from issuance.
- Storage: tokens are hashed (SHA-256) before storage. The plaintext token is returned only once during issuance.
- Revocation: tokens can be revoked from the web UI at
/settings/devices.
Token Management UI
Administrators can manage device tokens at Settings > Perangkat Terhubung (/settings/devices):
- View all active tokens (device name, client ID, last used, scopes).
- Revoke individual tokens.
- See when tokens were last used and from which IP.
No Authentication Required
The device code and token endpoints (/api/device/code and /api/device/token) do not require a Bearer token. They are the entry point for obtaining one.
Next: Quickstart
Quickstart
Complete working example from zero to first API call using curl. Replace https://balaka.example.com with your Balaka instance URL.
Prerequisites
- A running Balaka instance with at least one user account.
curlandjqinstalled.
1. Obtain an Access Token
# Step 1: Request a device code
RESPONSE=$(curl -s -X POST https://balaka.example.com/api/device/code \
-H "Content-Type: application/json" \
-d '{"clientId": "my-script"}')
DEVICE_CODE=$(echo $RESPONSE | jq -r '.deviceCode')
USER_CODE=$(echo $RESPONSE | jq -r '.userCode')
VERIFY_URL=$(echo $RESPONSE | jq -r '.verificationUriComplete')
echo "Open this URL in your browser: $VERIFY_URL"
echo "Or go to $(echo $RESPONSE | jq -r '.verificationUri') and enter code: $USER_CODE"
Open the URL in your browser, log in if needed, and click "Authorize".
# Step 2: Poll for the token (run after authorizing in browser)
TOKEN_RESPONSE=$(curl -s -X POST https://balaka.example.com/api/device/token \
-H "Content-Type: application/json" \
-d "{\"deviceCode\": \"$DEVICE_CODE\"}")
TOKEN=$(echo $TOKEN_RESPONSE | jq -r '.accessToken')
echo "Access token: $TOKEN"
If you get authorization_pending, wait 5 seconds and try again.
2. Get Trial Balance
curl -s https://balaka.example.com/api/analysis/trial-balance?asOfDate=2025-12-31 \
-H "Authorization: Bearer $TOKEN" | jq .
Response:
{
"reportType": "trial-balance",
"generatedAt": "2025-12-31T10:00:00",
"parameters": {
"asOfDate": "2025-12-31"
},
"data": {
"items": [
{
"accountCode": "1-1100",
"accountName": "Kas",
"debit": 50000000.00,
"credit": 0.00
}
],
"totalDebit": 150000000.00,
"totalCredit": 150000000.00
},
"metadata": {
"currency": "IDR",
"accountingBasis": "accrual",
"description": "Trial balance as of 2025-12-31..."
}
}
3. Create a Draft Transaction
First, find a template ID. Use the Swagger UI at /swagger-ui.html to browse available templates, or query the templates API.
# Create a draft transaction
curl -s -X POST https://balaka.example.com/api/drafts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"templateId": "e0000000-0000-0000-0000-000000000001",
"description": "Office supplies purchase",
"amount": 500000,
"transactionDate": "2025-12-15"
}' | jq .
Response:
{
"transactionId": "a1b2c3d4-...",
"transactionNumber": null,
"status": "DRAFT",
"merchant": null,
"amount": 500000,
"transactionDate": "2025-12-15",
"description": "Office supplies purchase",
"journalEntries": [
{
"journalNumber": null,
"accountCode": "5-1200",
"accountName": "Beban Perlengkapan Kantor",
"debitAmount": 500000.00,
"creditAmount": 0.00
},
{
"journalNumber": null,
"accountCode": "1-1100",
"accountName": "Kas",
"debitAmount": 0.00,
"creditAmount": 500000.00
}
]
}
4. Post the Transaction
TX_ID="a1b2c3d4-..." # from the response above
curl -s -X POST "https://balaka.example.com/api/transactions/$TX_ID/post" \
-H "Authorization: Bearer $TOKEN" | jq .
Response:
{
"transactionId": "a1b2c3d4-...",
"transactionNumber": "JU-202512-0001",
"status": "POSTED",
"merchant": null,
"amount": 500000,
"transactionDate": "2025-12-15",
"description": "Office supplies purchase",
"journalEntries": [...]
}
5. Verify in Trial Balance
curl -s https://balaka.example.com/api/analysis/trial-balance?asOfDate=2025-12-31 \
-H "Authorization: Bearer $TOKEN" | jq '.data.items[] | select(.accountCode == "5-1200")'
Full API Reference
Browse the interactive API documentation at /swagger-ui.html on your Balaka instance. All endpoints under /api/** are documented with request/response schemas and examples.
Next: Transactions
Transactions API
All transaction endpoints require Authorization: Bearer <token> with the transactions:post scope.
Base URL: /api/transactions
Transaction Lifecycle
DRAFT ──POST──> POSTED ──VOID──> VOID
│
└──DELETE──> (removed)
- DRAFT: created but not yet affecting the ledger. Can be edited or deleted.
- POSTED: affects the ledger. Journal entries are generated and numbered. Can only be voided.
- VOID: reversed. A reversal journal entry is created. Cannot be modified further.
Create Transaction (Direct Post)
Creates and immediately posts a transaction. Used by AI assistants after user approval.
POST /api/transactions
Content-Type: application/json
Authorization: Bearer <token>
{
"templateId": "e0000000-0000-0000-0000-000000000001",
"merchant": "Toko Bangunan",
"amount": 1500000,
"transactionDate": "2025-12-15",
"description": "Office renovation materials",
"category": "office-supplies",
"source": "claude-code",
"userApproved": true,
"accountSlots": {
"BANK": "648eaabb-1234-5678-9abc-def012345678"
},
"variables": {
"assetCost": 1500000
}
}
| Field | Type | Required | Description |
|---|---|---|---|
templateId | UUID | yes | Journal template to use |
merchant | string | yes | Merchant/vendor name |
amount | decimal | yes | Transaction amount (> 0) |
transactionDate | date | yes | Format: yyyy-MM-dd |
description | string | yes | Transaction description |
category | string | no | Optional category |
items | string[] | no | Line item descriptions |
source | string | no | Source identifier (e.g., claude-code) |
userApproved | boolean | no | Whether user approved the transaction |
accountSlots | map | no | Account overrides for template lines with accountHint. Key = hint or line order number, value = account UUID |
variables | map | no | Formula variables for DETAILED templates. Key = variable name, value = amount |
Response: 201 Created with TransactionResponse.
Create Draft Transaction
Creates a DRAFT transaction via the draft workflow.
POST /api/drafts
Content-Type: application/json
Authorization: Bearer <token>
{
"templateId": "e0000000-0000-0000-0000-000000000001",
"description": "Monthly electricity bill",
"amount": 750000,
"transactionDate": "2025-12-01",
"accountSlots": {
"BANK": "648eaabb-1234-5678-9abc-def012345678"
}
}
Response: 201 Created with TransactionResponse (status = DRAFT).
Create Free-Form Journal Entry
Creates a DRAFT transaction with arbitrary debit/credit lines. No template required. Use for closing journals, adjusting entries, or opening balances.
POST /api/transactions/journal-entry
Content-Type: application/json
Authorization: Bearer <token>
{
"transactionDate": "2025-12-31",
"description": "Closing journal - accrued expenses",
"category": "CLOSING",
"lines": [
{
"accountId": "a1b2c3d4-...",
"debit": 1000000,
"credit": 0
},
{
"accountId": "e5f6a7b8-...",
"debit": 0,
"credit": 1000000
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
transactionDate | date | yes | Format: yyyy-MM-dd |
description | string | yes | Journal entry description (max 500 chars) |
category | string | no | Optional category stored in transaction notes |
lines | array | yes | Minimum 2 lines. Total debits must equal total credits |
lines[].accountId | UUID | yes | Chart of account UUID (must be a leaf account, not a header) |
lines[].debit | decimal | yes | Debit amount (>= 0) |
lines[].credit | decimal | yes | Credit amount (>= 0) |
Response: 201 Created with TransactionResponse (status = DRAFT). Post via POST /api/transactions/{id}/post.
Validation errors (400 Bad Request):
- Unbalanced debits and credits.
- Less than 2 lines.
- Header account used (only leaf accounts allowed).
- A line has both debit and credit as zero or both non-zero.
Update Draft Transaction
Updates a DRAFT transaction. All fields are optional; only provided fields are updated.
PUT /api/transactions/{id}
Content-Type: application/json
Authorization: Bearer <token>
{
"templateId": "e0000000-0000-0000-0000-000000000002",
"description": "Updated description",
"amount": 800000,
"transactionDate": "2025-12-02",
"accountSlots": {
"BANK": "new-account-uuid"
}
}
Response: 200 OK with updated TransactionResponse.
Returns 409 Conflict if the transaction was modified concurrently (optimistic locking via row_version).
Post a Draft Transaction
POST /api/transactions/{id}/post
Authorization: Bearer <token>
Response: 200 OK with TransactionResponse (status = POSTED, transactionNumber assigned).
Delete a Draft Transaction
DELETE /api/transactions/{id}
Authorization: Bearer <token>
Response: 204 No Content. Only DRAFT transactions can be deleted.
Void a Posted Transaction
POST /api/transactions/{id}/void
Content-Type: application/json
Authorization: Bearer <token>
{
"reason": "DUPLICATE",
"notes": "Already recorded in transaction JU-202512-0001"
}
| Void Reason | Description |
|---|---|
INPUT_ERROR | Data entry mistake |
DUPLICATE | Duplicate transaction |
CANCELLED | Transaction was cancelled |
OTHER | Other reason (explain in notes) |
Response: 200 OK with TransactionResponse (status = VOID).
Journal Preview
Preview the journal entries that will be generated when posting a DRAFT transaction.
GET /api/transactions/{id}/journal-preview
Authorization: Bearer <token>
Response:
{
"valid": true,
"errors": [],
"entries": [
{
"accountCode": "5-1200",
"accountName": "Beban Perlengkapan Kantor",
"debitAmount": 500000.00,
"creditAmount": 0.00
},
{
"accountCode": "1-1100",
"accountName": "Kas",
"debitAmount": 0.00,
"creditAmount": 500000.00
}
],
"totalDebit": 500000.00,
"totalCredit": 500000.00
}
Bulk Post
Post multiple DRAFT transactions in a single request.
POST /api/transactions/bulk-post
Content-Type: application/json
Authorization: Bearer <token>
{
"transactionIds": [
"uuid-1",
"uuid-2",
"uuid-3"
]
}
Response:
{
"results": [
{ "transactionId": "uuid-1", "success": true, "transactionNumber": "JU-202512-0001", "error": null },
{ "transactionId": "uuid-2", "success": true, "transactionNumber": "JU-202512-0002", "error": null },
{ "transactionId": "uuid-3", "success": false, "transactionNumber": null, "error": "Transaction is not in DRAFT status" }
],
"successCount": 2,
"failureCount": 1
}
Individual failures do not roll back successful posts.
Purge Voided Transactions
Permanently delete all voided transactions (optionally before a cutoff date).
DELETE /api/transactions/purge-voided?before=2025-01-01
Authorization: Bearer <token>
Response: 200 OK with count of purged transactions.
Response Structure
All transaction endpoints return TransactionResponse:
{
"transactionId": "uuid",
"transactionNumber": "JU-202512-0001",
"status": "POSTED",
"merchant": "Toko Bangunan",
"amount": 1500000,
"transactionDate": "2025-12-15",
"description": "Office renovation materials",
"journalEntries": [
{
"journalNumber": "JE-202512-0001",
"accountCode": "5-1200",
"accountName": "Beban Perlengkapan Kantor",
"debitAmount": 1500000.00,
"creditAmount": 0.00
}
]
}
Next: Reports
Reports API
Financial report endpoints for reading accounting data. All endpoints require Authorization: Bearer <token> with the analysis:read scope.
Base URL: /api/analysis
Response Envelope
All report endpoints return an AnalysisResponse<T> wrapper:
{
"reportType": "trial-balance",
"generatedAt": "2025-12-31T10:00:00",
"parameters": {
"asOfDate": "2025-12-31"
},
"data": { ... },
"metadata": {
"currency": "IDR",
"accountingBasis": "accrual",
"description": "..."
}
}
| Field | Description |
|---|---|
reportType | Report identifier string |
generatedAt | ISO 8601 timestamp when the report was generated |
parameters | Echo of the request parameters |
data | Report-specific payload (varies per endpoint) |
metadata | Additional context (currency, descriptions) |
Company Info
GET /api/analysis/company
Returns company configuration: name, industry, currency, fiscal year start month, PKP status, NPWP.
Monthly Snapshot
GET /api/analysis/snapshot?month=2025-12&year=2025
Returns KPI dashboard data: revenue, expenses, net profit, profit margin, cash balance, receivables, payables, transaction count, and month-over-month change percentages.
Trial Balance
GET /api/analysis/trial-balance?asOfDate=2025-12-31
| Parameter | Type | Required | Description |
|---|---|---|---|
asOfDate | date | yes | Balance date (yyyy-MM-dd) |
Response data:
{
"items": [
{
"accountCode": "1-1100",
"accountName": "Kas",
"debit": 50000000.00,
"credit": 0.00
}
],
"totalDebit": 150000000.00,
"totalCredit": 150000000.00
}
Income Statement
GET /api/analysis/income-statement?startDate=2025-01-01&endDate=2025-12-31&excludeClosing=true
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | date | yes | Period start (yyyy-MM-dd) |
endDate | date | yes | Period end (yyyy-MM-dd) |
excludeClosing | boolean | no | Exclude closing journal entries (default: false) |
Response data:
{
"revenueItems": [
{ "accountCode": "4-1100", "accountName": "Pendapatan Jasa", "amount": 120000000.00 }
],
"expenseItems": [
{ "accountCode": "5-1100", "accountName": "Beban Gaji", "amount": 60000000.00 }
],
"totalRevenue": 120000000.00,
"totalExpense": 80000000.00,
"netIncome": 40000000.00
}
Balance Sheet
GET /api/analysis/balance-sheet?asOfDate=2025-12-31
| Parameter | Type | Required | Description |
|---|---|---|---|
asOfDate | date | yes | Balance date (yyyy-MM-dd) |
Response data:
{
"assetItems": [
{ "accountCode": "1-1100", "accountName": "Kas", "amount": 50000000.00 }
],
"liabilityItems": [
{ "accountCode": "2-1100", "accountName": "Hutang Usaha", "amount": 10000000.00 }
],
"equityItems": [
{ "accountCode": "3-1100", "accountName": "Modal Disetor", "amount": 100000000.00 }
],
"totalAssets": 150000000.00,
"totalLiabilities": 10000000.00,
"totalEquity": 100000000.00,
"currentYearEarnings": 40000000.00
}
The accounting equation holds: totalAssets = totalLiabilities + totalEquity + currentYearEarnings.
Cash Flow Statement
GET /api/analysis/cash-flow?startDate=2025-01-01&endDate=2025-12-31
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | date | yes | Period start (yyyy-MM-dd) |
endDate | date | yes | Period end (yyyy-MM-dd) |
Response data:
{
"operatingItems": [
{ "description": "Pendapatan Jasa", "amount": 120000000.00 }
],
"investingItems": [
{ "description": "Pembelian Aset Tetap", "amount": -15000000.00 }
],
"financingItems": [],
"operatingTotal": 40000000.00,
"investingTotal": -15000000.00,
"financingTotal": 0.00,
"netCashChange": 25000000.00,
"beginningCashBalance": 25000000.00,
"endingCashBalance": 50000000.00,
"cashAccountBalances": [
{ "accountName": "Bank BCA", "balance": 45000000.00 },
{ "accountName": "Kas", "balance": 5000000.00 }
]
}
Positive amounts represent cash inflows, negative amounts represent cash outflows. Cash flow categories are determined by the cashFlowCategory field on journal templates (OPERATING, INVESTING, FINANCING, NON_CASH).
Tax Summary
GET /api/analysis/tax-summary?startDate=2025-01-01&endDate=2025-12-31
Returns aggregated tax data for the period: PPN, PPh 21, PPh 23, PPh 4(2), PPh 25.
Additional Endpoints
| Endpoint | Description |
|---|---|
GET /api/analysis/accounts | Chart of accounts with balances |
GET /api/analysis/transactions/{id} | Single transaction with full journal entries |
GET /api/analysis/ledger | General ledger entries for a date range |
For the full list of available endpoints and their schemas, see the Swagger UI at /swagger-ui.html.
Next: Payroll
Payroll API
Payroll management, PPh 21 calculation, and 1721-A1 generation. All endpoints require Authorization: Bearer <token> with the tax-export:read scope.
Base URL: /api/payroll
Payroll Run Lifecycle
DRAFT ──calculate──> CALCULATED ──approve──> APPROVED ──post──> POSTED
│ │
└──delete └──recalculate (back to CALCULATED)
- DRAFT: payroll run created, no calculations yet.
- CALCULATED: BPJS and PPh 21 computed for all active employees.
- APPROVED: reviewed and approved, ready for posting.
- POSTED: journal entry created. Payment is recorded. Payroll data appears in summary and 1721-A1 endpoints.
List Payroll Runs
GET /api/payroll?year=2025&status=POSTED&page=0&size=20
| Parameter | Type | Required | Description |
|---|---|---|---|
year | int | no | Filter by year |
status | enum | no | Filter by status: DRAFT, CALCULATED, APPROVED, POSTED |
page | int | no | Page number (0-based) |
size | int | no | Page size |
Response: paginated PayrollRunResponse list.
Create Payroll Run
POST /api/payroll
Content-Type: application/json
{
"payrollPeriod": "2025-12",
"notes": "December 2025 payroll"
}
Response: 201 Created
{
"id": "uuid",
"payrollPeriod": "2025-12",
"status": "DRAFT",
"employeeCount": 0,
"totalGross": 0,
"totalDeductions": 0,
"totalNet": 0,
"totalPph21": 0,
"notes": "December 2025 payroll"
}
Only one payroll run per period is allowed. Returns 400 if a run already exists for that period.
Get Payroll Run Detail
GET /api/payroll/{id}
Returns the payroll run with all employee payroll details (gross salary, deductions, PPh 21, net salary per employee).
Calculate Payroll
POST /api/payroll/{id}/calculate
Content-Type: application/json
{
"baseSalary": 5000000,
"jkkRiskClass": 2
}
| Field | Type | Required | Description |
|---|---|---|---|
baseSalary | decimal | yes | Base salary for calculation |
jkkRiskClass | int | no | JKK risk class 1-5 (default: 1) |
Calculates BPJS contributions and PPh 21 for all active employees. PPh 21 uses TER method (PMK 168/2023) for January--November. December uses annual reconciliation with progressive tax brackets (PP 58/2023).
Transitions status to CALCULATED. Returns 400 if no active employees exist.
Approve Payroll
POST /api/payroll/{id}/approve
Transitions from CALCULATED to APPROVED. Returns 400 if not in CALCULATED status.
Post Payroll
POST /api/payroll/{id}/post
Creates a journal entry using the payroll template (configured via app.payroll.template-id). Template lines with zero amounts are skipped (e.g., PPh 21 = 0 for low-salary employees). Transitions to POSTED.
Returns 500 if the payroll template is not found.
Delete Payroll Run
DELETE /api/payroll/{id}
Only DRAFT payroll runs can be deleted. Response: 204 No Content.
1721-A1 (Employee Tax Proof)
GET /api/payroll/employees/{employeeId}/1721-a1?year=2025
Generates annual PPh 21 tax proof (bukti potong) data for a single employee. Only includes data from POSTED payroll runs.
Response:
{
"year": 2025,
"employee": {
"name": "John Doe",
"npwp": "12.345.678.9-012.000",
"nikKtp": "1234567890123456",
"ptkpStatus": "TK_0",
"hireDate": "2024-01-15",
"resignDate": null,
"monthCount": 12
},
"calculation": {
"penghasilanBruto": 72000000,
"biayaJabatan": 3600000,
"penghasilanNeto": 68400000,
"ptkp": 54000000,
"pkp": 14400000,
"pph21Terutang": 720000,
"pph21Dipotong": 720000,
"pph21KurangBayar": 0
},
"monthlyBreakdown": [
{ "period": "2025-01", "grossSalary": 6000000, "pph21": 60000 },
{ "period": "2025-02", "grossSalary": 6000000, "pph21": 60000 }
]
}
Returns 404 if the employee has no POSTED payroll data for the given year.
PPh 21 Annual Summary
GET /api/payroll/pph21/summary?year=2025
Aggregates PPh 21 data across all employees from POSTED payroll runs. Returns total gross, total PPh 21, and per-employee breakdown.
Payroll Schedule (Automated Runs)
Get Schedule
GET /api/payroll/schedule
Returns the current payroll schedule configuration, or 404 if not configured.
Create/Update Schedule
POST /api/payroll/schedule
Content-Type: application/json
{
"dayOfMonth": 25,
"baseSalary": 5000000,
"jkkRiskClass": 2,
"autoCalculate": true,
"autoApprove": false,
"active": true
}
| Field | Type | Required | Description |
|---|---|---|---|
dayOfMonth | int | yes | Day of month to create payroll (1-28) |
baseSalary | decimal | yes | Base salary for auto-calculation |
jkkRiskClass | int | yes | JKK risk class (1-5) |
autoCalculate | boolean | yes | Automatically calculate after creation |
autoApprove | boolean | no | Automatically approve after calculation |
active | boolean | no | Enable/disable the schedule (default: true) |
Only one schedule exists at a time. POST replaces the existing configuration. The scheduler runs daily at 6:30 AM and creates a DRAFT payroll run if the day matches dayOfMonth. Posting is always manual -- it signifies actual payment.
Delete Schedule
DELETE /api/payroll/schedule
Response: 204 No Content.
Next: Tax Export
Tax Export API
Endpoints for exporting tax data for SPT preparation. Supports both JSON and Excel (XLSX) output formats. All endpoints require Authorization: Bearer <token> with the tax-export:read scope.
Base URL: /api/tax-export
Coretax Excel Exports
These endpoints produce Coretax-compatible Excel files for direct import.
e-Faktur Keluaran (Output VAT)
GET /api/tax-export/efaktur-keluaran?startMonth=2025-01&endMonth=2025-12
e-Faktur Masukan (Input VAT)
GET /api/tax-export/efaktur-masukan?startMonth=2025-01&endMonth=2025-12
e-Bupot Unifikasi (PPh Withholding)
GET /api/tax-export/bupot-unifikasi?startMonth=2025-01&endMonth=2025-12
All three accept startMonth and endMonth in yyyy-MM format. Response is an Excel file download (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet).
JSON / Excel Dual-Format Endpoints
These endpoints return JSON by default. Add ?format=excel to download an XLSX file instead.
PPN Detail Report
GET /api/tax-export/ppn-detail?startDate=2025-01-01&endDate=2025-12-31
GET /api/tax-export/ppn-detail?startDate=2025-01-01&endDate=2025-12-31&format=excel
PPN detail with Faktur Keluaran/Masukan breakdown, DPP and PPN totals.
PPh 23 Detail Report
GET /api/tax-export/pph23-detail?startDate=2025-01-01&endDate=2025-12-31
GET /api/tax-export/pph23-detail?startDate=2025-01-01&endDate=2025-12-31&format=excel
Fiscal Reconciliation
GET /api/tax-export/rekonsiliasi-fiskal?year=2025
GET /api/tax-export/rekonsiliasi-fiskal?year=2025&format=excel
Commercial income to taxable income (PKP) reconciliation with positive/negative fiscal adjustments.
PPh Badan (Corporate Income Tax)
GET /api/tax-export/pph-badan?year=2025
JSON only. Returns PKP, PPh terutang, kredit pajak (PPh 23/25), and PPh 29 calculation including Pasal 31E facility.
SPT Tahunan Badan Exports
Annual corporate tax return data in Coretax format. All accept year parameter and optional format=excel.
Lampiran I -- Fiscal Reconciliation
GET /api/tax-export/spt-tahunan/l1?year=2025
GET /api/tax-export/spt-tahunan/l1?year=2025&format=excel
Lampiran IV -- Final Income (PPh 4(2))
GET /api/tax-export/spt-tahunan/l4?year=2025
GET /api/tax-export/spt-tahunan/l4?year=2025&format=excel
Transkrip 8A -- Financial Statements
GET /api/tax-export/spt-tahunan/transkrip-8a?year=2025
GET /api/tax-export/spt-tahunan/transkrip-8a?year=2025&format=excel
Balance sheet + income statement in Coretax 8A layout.
Lampiran 9 -- Depreciation & Amortization
GET /api/tax-export/spt-tahunan/l9?year=2025
GET /api/tax-export/spt-tahunan/l9?year=2025&format=excel
Fixed asset depreciation in DJP converter format.
e-Bupot PPh 21 Annual (1721-A1)
GET /api/tax-export/ebupot-pph21?year=2025
GET /api/tax-export/ebupot-pph21?year=2025&format=excel
Annual PPh 21 reconciliation for all employees. Excel output uses DJP BPA1 converter format.
Consolidated Lampiran
GET /api/tax-export/spt-tahunan/lampiran?year=2025
JSON only. Returns all lampiran data (L1, L4, L9, Transkrip 8A, BPA1) mapped to Coretax field numbers in a single response (SptLampiranReport).
Financial Statements PDF
GET /api/tax-export/financial-statements/pdf?year=2025
Combined Balance Sheet and Income Statement as a PDF file for Coretax SPT upload. Response content type: application/pdf.
Coretax SPT Badan Export
GET /api/tax-export/coretax/spt-badan?year=2025
Structured JSON matching Coretax form fields. All values are plain numbers for direct entry into the Coretax web form (CoretaxSptBadanExport).
Response:
{
"reportType": "coretax-spt-badan",
"generatedAt": "2025-12-31T10:00:00",
"parameters": { "year": "2025" },
"data": {
...
},
"metadata": {
"description": "Coretax-compatible SPT Badan export...",
"currency": "IDR"
}
}
Date Format Reference
| Parameter | Format | Example |
|---|---|---|
startMonth, endMonth | yyyy-MM | 2025-01 |
startDate, endDate | yyyy-MM-dd | 2025-01-01 |
year | integer | 2025 |
Next: Pagination
Pagination
List endpoints that return collections use Spring Data's standard pagination. This applies to payroll runs, transactions, and other list APIs.
Request Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 0 | Page number (0-based) |
size | int | 20 | Items per page |
sort | string | varies | Sort field and direction (e.g., transactionDate,desc) |
Sort Syntax
The sort parameter accepts a property name and optional direction:
sort=propertyName,asc (ascending)
sort=propertyName,desc (descending)
sort=propertyName (ascending by default)
Multiple sort fields can be specified by repeating the parameter:
GET /api/payroll?sort=payrollPeriod,desc&sort=status,asc
Response Envelope
Paginated responses use Spring Data's Page structure:
{
"content": [
{ "id": "uuid-1", "payrollPeriod": "2025-12", "status": "POSTED", ... },
{ "id": "uuid-2", "payrollPeriod": "2025-11", "status": "POSTED", ... }
],
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalElements": 42,
"totalPages": 3,
"size": 20,
"number": 0,
"numberOfElements": 20,
"first": true,
"last": false,
"empty": false,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
}
}
| Field | Description |
|---|---|
content | Array of items for the current page |
totalElements | Total number of items across all pages |
totalPages | Total number of pages |
number | Current page number (0-based) |
size | Requested page size |
numberOfElements | Actual number of items in this page |
first | true if this is the first page |
last | true if this is the last page |
empty | true if the page has no content |
Example: Iterate All Pages
PAGE=0
TOTAL_PAGES=1
while [ $PAGE -lt $TOTAL_PAGES ]; do
RESPONSE=$(curl -s "https://balaka.example.com/api/payroll?page=$PAGE&size=10&sort=payrollPeriod,desc" \
-H "Authorization: Bearer $TOKEN")
echo "$RESPONSE" | jq '.content[]'
TOTAL_PAGES=$(echo "$RESPONSE" | jq '.totalPages')
PAGE=$((PAGE + 1))
done
Endpoints Using Pagination
| Endpoint | Default Sort |
|---|---|
GET /api/payroll | by payroll period |
GET /api/analysis/accounts | by account code |
GET /api/bills | by bill date |
Non-paginated endpoints (reports, single-entity lookups) return their data directly without the Page wrapper.
Previous: Tax Export