Documentation

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
    }
}
Currency-aware tax label

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.

Optional fields fall back to project settings

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 — explicit invoiceId doesn't exist. Body: { "error": "invoice_not_found", ... }.
  • 400 Bad Request — explicit invoiceId belongs to a different project. Body: { "error": "invoice_project_mismatch", ... }.
  • 500 Internal Server Error — render failed. Body: { "error": "render_failed", "invoiceId": 5678, "message": "..." }.