Invoices API
All invoice endpoints require JWT authentication.
Prefill (Preview)
Generate an invoice preview from ClickHouse data without saving.
POST /api/v1/invoices/prefill
Request
{
"application_key": "APP-001",
"period_start": "2026-02-01",
"period_end": "2026-02-28"
}
period_start and period_end are optional — defaults to the previous calendar month.
Response (200)
{
"data": {
"application_key": "APP-001",
"customer_key": "CUST-001",
"period_start": "2026-02-01",
"period_end": "2026-02-28",
"deal_type": "SUP",
"currency": "UZS",
"base_amount": "15000000.00",
"overtime_hours": "5.50",
"overtime_amount": "825000.00",
"total_amount": "15825000.00",
"total_hours": "45.50",
"is_overtime": true,
"line_items": [ ... ],
"customer": { ... },
"application": { ... }
}
}
Generate (Create Draft)
Save a draft invoice to PostgreSQL.
POST /api/v1/invoices/generate
Request
{
"application_key": "APP-001",
"period_start": "2026-02-01",
"period_end": "2026-02-28",
"deal_type": "SUP",
"currency": "UZS",
"base_amount": "15000000.00",
"total_amount": "15825000.00",
"total_hours": "45.50",
"overtime_hours": "5.50",
"overtime_amount": "825000.00",
"is_overtime": true,
"line_items": [
{
"line_type": "support",
"description": "Monthly support — February 2026",
"time_seconds": 0,
"billable_seconds": 0,
"unit_price": "15000000.00",
"quantity": "1",
"line_total": "15000000.00",
"rate_tier": "standard",
"rate_multiplier": "1.0",
"source": "auto",
"is_auto_generated": true,
"sort_order": 0
}
]
}
Response (201)
Returns the saved invoice with its generated id and display_number.
Generate Bulk
Create draft invoices for all active applications in a period.
POST /api/v1/invoices/generate-bulk
Request
{
"period_start": "2026-02-01",
"period_end": "2026-02-28"
}
Response (200)
{
"data": [
{ "id": "uuid-1", "application_key": "APP-001", "status": "draft", ... },
{ "id": "uuid-2", "application_key": "APP-002", "status": "draft", ... }
]
}
List Invoices
GET /api/v1/invoices
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (draft, needs_review, etc.) |
application_key | string | Filter by application |
page | int | Page number (default: 1) |
per_page | int | Items per page (default: 20, max: 100) |
Example
curl "http://localhost:8081/api/v1/invoices?status=draft&page=1&per_page=10" \
-H "Authorization: Bearer $TOKEN"
Response (200)
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"display_number": "INV-1",
"application_key": "APP-001",
"customer_key": "CUST-001",
"period_start": "2026-02-01",
"period_end": "2026-02-28",
"deal_type": "SUP",
"currency": "UZS",
"total_amount": "15825000.00",
"status": "draft",
"created_at": "2026-02-15T10:00:00Z"
}
],
"meta": {
"total": 42,
"page": 1,
"per_page": 10
}
}
Get Invoice
GET /api/v1/invoices/:id
Response (200)
Returns the invoice with all line items included.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"display_number": "INV-1",
"application_key": "APP-001",
"customer_key": "CUST-001",
"period_start": "2026-02-01",
"period_end": "2026-02-28",
"deal_type": "SUP",
"currency": "UZS",
"base_amount": "15000000.00",
"overtime_hours": "5.50",
"overtime_amount": "825000.00",
"total_amount": "15825000.00",
"total_hours": "45.50",
"is_overtime": true,
"status": "draft",
"version": 1,
"created_at": "2026-02-15T10:00:00Z",
"updated_at": "2026-02-15T10:00:00Z",
"line_items": [
{
"id": "item-uuid",
"line_type": "task",
"description": "PROJ-123: Fix login bug",
"jira_key": "PROJ-123",
"time_seconds": 3600,
"billable_seconds": 3600,
"unit_price": "150000.00",
"quantity": "1.00",
"line_total": "150000.00",
"rate_tier": "standard",
"rate_multiplier": "1.0",
"source": "auto",
"is_auto_generated": true,
"sort_order": 1,
"linked_issue_key": "SUP-123",
"worklog_date": "2026-02-03"
}
]
}
}
Update Invoice
Update invoice-level fields (only in draft status).
PATCH /api/v1/invoices/:id
Request
{
"base_amount": "16000000.00",
"total_amount": "16825000.00",
"invoice_date": "2026-03-01",
"show_task_list": false,
"show_overtime": true,
"legal_text": "Custom legal paragraph..."
}
All fields are optional. Editable fields: base_amount, total_amount, overtime_hours, overtime_amount, is_overtime, invoice_date, show_task_list, show_tracked_time, show_task_pricing, show_overtime, legal_text, html_override.
Response (200)
Returns the updated invoice.
Update Line Items
Add, update, or remove line items in a single request.
PATCH /api/v1/invoices/:id/line-items
Request
{
"add": [
{
"line_type": "task",
"description": "Additional work",
"time_seconds": 7200,
"billable_seconds": 7200,
"unit_price": "150000.00",
"quantity": "2.00",
"line_total": "300000.00",
"rate_tier": "standard",
"rate_multiplier": "1.0",
"source": "manual",
"is_auto_generated": false,
"sort_order": 10
}
],
"update": [
{
"id": "existing-line-uuid",
"description": "Updated description",
"line_total": "500000.00"
}
],
"remove": [
"line-uuid-to-remove"
]
}
Response (200)
Returns the invoice with updated line items.
Finalize
Generate PDF and transition to needs_review.
POST /api/v1/invoices/:id/finalize
No request body. Only works on draft invoices.
Response (200)
Returns the invoice with status: "needs_review".
Errors
| Status | Code | Cause |
|---|---|---|
| 409 | CONFLICT | Invoice not in draft status |
| 500 | INTERNAL_ERROR | PDF generation failed |
Regenerate
Re-fetch data from ClickHouse and reset to draft.
PUT /api/v1/invoices/:id/regenerate
No request body. Works on declined, rejected, or approved invoices.
Response (200)
Returns the regenerated invoice with status: "draft".
Get Edit History
GET /api/v1/invoices/:id/edit-history
Response (200)
{
"data": [
{
"id": "history-uuid",
"invoice_id": "invoice-uuid",
"line_item_id": "line-uuid",
"edit_type": "line_modified",
"field_name": "line_total",
"old_value": "300000.00",
"new_value": "500000.00",
"edited_by": "admin",
"edited_at": "2026-02-15T11:00:00Z"
}
]
}
Preview HTML
Render the invoice as server-side HTML for fast draft preview (~200ms). Used by the frontend to show a live preview in a sandboxed iframe.
GET /api/v1/invoices/:id/preview-html
Response (200)
Returns Content-Type: text/html with the rendered invoice HTML. If the invoice has an html_override set, returns that directly instead of rendering from the template. Uses the same template engine as PDF generation but skips PDF conversion.
curl http://localhost:8081/api/v1/invoices/{id}/preview-html \
-H "Authorization: Bearer $TOKEN"
Sync PlanFact
Manually retry PlanFact shipment sync for an approved or sent invoice.
POST /api/v1/invoices/:id/sync-planfact
No request body. Only works on approved or sent invoices.
Response (200)
{
"data": {
"message": "planfact sync complete"
}
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid UUID, wrong status, or PlanFact not configured |
| 404 | NOT_FOUND | Invoice not found |
Download PDF
GET /api/v1/invoices/:id/pdf
Response (200)
Returns the PDF binary with Content-Type: application/pdf. Returns 404 if no PDF exists (invoice not yet finalized).
curl http://localhost:8081/api/v1/invoices/{id}/pdf \
-H "Authorization: Bearer $TOKEN" \
-o invoice.pdf