Invoices API
Create and manage saved invoices for projects.
GET /api/v1/projects/{projectId}/invoices
List all saved invoices for a project.
Response
{
"success": true,
"data": {
"projectId": 123,
"projectName": "Corporate HQ",
"currency": "USD",
"taxLabel": "Tax",
"invoices": [
{
"id": 1,
"invoiceNumber": "INV-2024-001",
"purchaseOrderNumber": "PO-5678",
"invoiceDate": "2024-12-15",
"dueDate": "2025-01-14",
"paymentTerms": "Net 30",
"totalAmount": 45250.00,
"currency": "USD",
"taxRate": 8.250,
"createdAt": "2024-12-15T10:30:00Z",
"lastGeneratedAt": "2024-12-20T14:00:00Z"
}
],
"totalCount": 3
}
}
taxLabel tracks the project's currency: USD returns "Tax", GBP/EUR return "VAT", CAD/AUD/INR return "GST", JPY returns "Consumption Tax". Per-invoice taxRate is sourced from the linked Contract's frozen rate snapshot (legacy invoices with no contract surface 0.000).
POST /api/v1/projects/{projectId}/invoices
Save a new invoice. This generates the invoice and stores the configuration for future regeneration.
Omit includeTaxes, showNonEquipment, paymentTerms, or paymentInstructions to inherit the project's saved Invoice preferences. Pass an explicit value to override. Final fallback (no caller value, no project setting) is the system default — taxes off, non-equipment shown, payment terms "Due on Receipt".
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
invoiceNumber |
string | Yes | Invoice number (max 100 chars) |
purchaseOrderNumber |
string | No | Customer purchase order reference |
invoiceDate |
date | No | Invoice date in YYYY-MM-DD format (default: today) |
dueDate |
date | No | Payment due date (default: 30 days from invoice date) |
paymentTerms |
string | No | Payment terms text (default: "Due on Receipt") |
paymentInstructions |
string | No | Payment instructions (wire transfer details, etc.) |
showNonEquipment |
boolean | No | Include labor, programming, freight line items (default: true) |
includeTaxes |
boolean | No | Include tax calculations (default: false) |
roomIds |
array | No | Array of room IDs to include (default: all non-AddAlt rooms) |
Example Request
{
"invoiceNumber": "INV-2024-002",
"purchaseOrderNumber": "PO-9012",
"invoiceDate": "2024-12-20",
"dueDate": "2025-01-19",
"paymentTerms": "Net 30",
"paymentInstructions": "Wire transfer to...",
"showNonEquipment": true,
"includeTaxes": true,
"roomIds": [1, 2, 3]
}
Response (201 Created)
{
"success": true,
"data": {
"invoiceId": 2,
"projectId": 123,
"invoiceNumber": "INV-2024-002",
"totalAmount": 52750.00,
"currency": "USD",
"taxLabel": "Tax",
"taxRate": 8.250
}
}
GET /api/v1/invoices/{invoiceId}
Get a saved invoice with regenerated HTML content.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
regenerate |
boolean | true | Whether to regenerate the invoice HTML with current data |
Response
{
"success": true,
"data": {
"invoice": {
"id": 1,
"projectId": 123,
"projectName": "Corporate HQ",
"invoiceNumber": "INV-2024-001",
"totalAmount": 45250.00,
"currency": "USD",
"taxRate": 8.250,
...
},
"currency": "USD",
"taxLabel": "Tax",
"html": "<!DOCTYPE html><html>...invoice HTML...</html>"
}
}
DELETE /api/v1/invoices/{invoiceId}
Delete a saved invoice. Refuses to delete an invoice that still has live (un-voided) billing allocations — void it first via POST /api/v1/invoices/{invoiceId}/void. Requires write access.
Response
200 OK— deleted successfully (body below).409 Conflict— invoice still has live allocations. Void it first.404 Not Found— invoice does not exist for your tenant.403 Forbidden— your role is read-only.
200 Response Body
{
"success": true,
"data": {
"invoiceId": 1,
"invoiceNumber": "INV-2024-001",
"projectId": 123,
"message": "Invoice deleted successfully"
}
}
409 Response Body
{
"error": "invoice_has_live_allocations",
"message": "Cannot delete an invoice with live allocations. Void the invoice first."
}
POST /api/v1/invoices/{invoiceId}/void
Void a saved invoice — rolls back its billing allocations so the contract's billed-to-date drops and the invoice can then be deleted. Idempotent: voiding an already-voided invoice is a no-op success. Requires write access.
Request Body (optional)
{
"reason": "Billed the wrong milestone"
}
If reason is omitted, the void is recorded as "Voided via API" in the audit trail.
Response
200 OK— voided successfully (body below).404 Not Found— invoice does not exist for your tenant.403 Forbidden— your role is read-only.
200 Response Body
{
"success": true,
"invoiceId": 1,
"message": "Invoice voided successfully"
}
404 Response Body
{
"success": false,
"error": {
"code": "INVOICE_NOT_FOUND",
"message": "Invoice with ID 1 not found",
"details": null
},
"timestamp": "2026-05-30T00:00:00Z"
}
POST /api/v1/projects/{projectId}/active-version-invoice-html
Returns the rendered HTML of the project's latest saved invoice (or an explicit invoiceId if supplied). HTML comes from the SavedInvoice's stored snapshot. The invoice resolver does NOT filter by version (saved invoices link to Contract, not directly to Version) — so this returns the latest invoice across the project regardless of active version.
Request body (all fields optional)
{
"invoiceId": 5678
}
Response (200 OK)
{
"success": true,
"projectId": 42,
"invoiceId": 5678,
"invoiceNumber": "INV-2026-005",
"html": "<!DOCTYPE html><html>...</html>"
}
Errors
404 Not Found— no saved invoice. Body:{ "error": "no_saved_invoice", ... }.404 Not Found— explicitinvoiceIddoesn't exist. Body:{ "error": "invoice_not_found", ... }.400 Bad Request— explicitinvoiceIdbelongs to a different project. Body:{ "error": "invoice_project_mismatch", ... }.500 Internal Server Error— render failed. Body:{ "error": "render_failed", "invoiceId": 5678, "message": "..." }.
