Balaka Developer: API Guide

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):

errormeaning
authorization_pendingUser has not yet authorized. Keep polling.
expired_tokenDevice code expired (15 min). Restart from Step 1.
access_deniedUser denied authorization.
invalid_requestInvalid 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:

ScopeGrants access to
drafts:createCreate draft transactions
drafts:approveApprove/reject drafts
drafts:readRead draft transactions
analysis:readFinancial reports, snapshots, trial balance, balance sheet, income statement, cash flow
analysis:writePublish AI analysis reports
transactions:postCreate, update, post, void, delete transactions; free-form journal entries
data:importImport industry seed data
bills:readRead vendor bills
bills:createCreate vendor bills
bills:approveApprove vendor bills
tax-export:readTax reports, SPT data, payroll, employees, fiscal adjustments, salary components
accounts:readRead chart of accounts
accounts:writeCreate/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.
  • curl and jq installed.

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
  }
}
FieldTypeRequiredDescription
templateIdUUIDyesJournal template to use
merchantstringyesMerchant/vendor name
amountdecimalyesTransaction amount (> 0)
transactionDatedateyesFormat: yyyy-MM-dd
descriptionstringyesTransaction description
categorystringnoOptional category
itemsstring[]noLine item descriptions
sourcestringnoSource identifier (e.g., claude-code)
userApprovedbooleannoWhether user approved the transaction
accountSlotsmapnoAccount overrides for template lines with accountHint. Key = hint or line order number, value = account UUID
variablesmapnoFormula 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
    }
  ]
}
FieldTypeRequiredDescription
transactionDatedateyesFormat: yyyy-MM-dd
descriptionstringyesJournal entry description (max 500 chars)
categorystringnoOptional category stored in transaction notes
linesarrayyesMinimum 2 lines. Total debits must equal total credits
lines[].accountIdUUIDyesChart of account UUID (must be a leaf account, not a header)
lines[].debitdecimalyesDebit amount (>= 0)
lines[].creditdecimalyesCredit 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 ReasonDescription
INPUT_ERRORData entry mistake
DUPLICATEDuplicate transaction
CANCELLEDTransaction was cancelled
OTHEROther 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": "..."
  }
}
FieldDescription
reportTypeReport identifier string
generatedAtISO 8601 timestamp when the report was generated
parametersEcho of the request parameters
dataReport-specific payload (varies per endpoint)
metadataAdditional 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
ParameterTypeRequiredDescription
asOfDatedateyesBalance 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
ParameterTypeRequiredDescription
startDatedateyesPeriod start (yyyy-MM-dd)
endDatedateyesPeriod end (yyyy-MM-dd)
excludeClosingbooleannoExclude 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
ParameterTypeRequiredDescription
asOfDatedateyesBalance 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
ParameterTypeRequiredDescription
startDatedateyesPeriod start (yyyy-MM-dd)
endDatedateyesPeriod 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

EndpointDescription
GET /api/analysis/accountsChart of accounts with balances
GET /api/analysis/transactions/{id}Single transaction with full journal entries
GET /api/analysis/ledgerGeneral 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
ParameterTypeRequiredDescription
yearintnoFilter by year
statusenumnoFilter by status: DRAFT, CALCULATED, APPROVED, POSTED
pageintnoPage number (0-based)
sizeintnoPage 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
}
FieldTypeRequiredDescription
baseSalarydecimalyesBase salary for calculation
jkkRiskClassintnoJKK 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
}
FieldTypeRequiredDescription
dayOfMonthintyesDay of month to create payroll (1-28)
baseSalarydecimalyesBase salary for auto-calculation
jkkRiskClassintyesJKK risk class (1-5)
autoCalculatebooleanyesAutomatically calculate after creation
autoApprovebooleannoAutomatically approve after calculation
activebooleannoEnable/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

ParameterFormatExample
startMonth, endMonthyyyy-MM2025-01
startDate, endDateyyyy-MM-dd2025-01-01
yearinteger2025

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

ParameterTypeDefaultDescription
pageint0Page number (0-based)
sizeint20Items per page
sortstringvariesSort 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
  }
}
FieldDescription
contentArray of items for the current page
totalElementsTotal number of items across all pages
totalPagesTotal number of pages
numberCurrent page number (0-based)
sizeRequested page size
numberOfElementsActual number of items in this page
firsttrue if this is the first page
lasttrue if this is the last page
emptytrue 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

EndpointDefault Sort
GET /api/payrollby payroll period
GET /api/analysis/accountsby account code
GET /api/billsby bill date

Non-paginated endpoints (reports, single-entity lookups) return their data directly without the Page wrapper.


Previous: Tax Export