Brontiq Invoice API

REST API for LLM-driven invoice management via Claude Code or curl

Quick Start

Set your API key in your environment, then use curl to manage invoices:

export BRONTIQ_API_KEY=your_api_key_here
export BRONTIQ_URL=https://www.brontiqbooks.com

# Find a client by name, spelled roughly
curl -s "$BRONTIQ_URL/api/invoice?action=find_client&name=ravensworth" \
  -H "Authorization: Bearer $BRONTIQ_API_KEY"

# Raise an invoice against the matched client
curl -X POST "$BRONTIQ_URL/api/invoice" \
  -H "Authorization: Bearer $BRONTIQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"client_name":"Ravensworth Civil","invoice_number":"BB-2026-0197","due_date":"2026-04-09"}'

LLM Reference (Machine-Readable)

This section is optimised for LLMs parsing the page content.

AUTH: Bearer token, compared in constant time. 401 on absent, malformed or wrong.
CLIENT MATCHING: exact, then contains either way, then word overlap at >= 0.5.
           No match is refused, never silently creates a second client.

POST   /api/invoice                      -> raise invoice, client matched by name
PATCH  /api/invoice/{id}                 -> update permitted fields only
POST   /api/invoice/{id}/line-items      -> add line, recompute parent totals
DELETE /api/invoice/{id}                 -> soft delete through deletion_log

LINE ITEM FIELDS: item_name*, quantity*, unit_price*, description,
                  gst_percent (default 10), is_hourly, unbillable, activity_date

TOTALS: line_total summed; GST summed per line at that line's own gst_percent;
        both rounded once. GST is NOT 10% applied at the foot.

STATUS: draft | due | outstanding | paid. "overdue" is NOT storable; it is
        derived at read time from due_date and is false for paid invoices.
        Setting status=paid stamps paid_date; moving off paid clears it.

The one call that behaves unexpectedly

Rate and hours alone Total holds
$ curl -X PATCH "$BRONTIQ_URL/api/invoice/inv_8f2c" \
    -H "Authorization: Bearer $BRONTIQ_API_KEY" \
    -d '{"hourly_quantity": 42}'

{
  "id": "inv_8f2c",
  "invoice_number": "BB-2026-0197",
  "is_hourly": true,
  "hourly_rate": 165,
  "hourly_quantity": 42,
  "amount": 8300,
  "gst": 830,
  "total": 9130
}
The hours changed. The money did not. is_hourly is true on the stored record, but the recalculation never looked there.
Billing mode named in the body Recalculates
$ curl -X PATCH "$BRONTIQ_URL/api/invoice/inv_8f2c" \
    -H "Authorization: Bearer $BRONTIQ_API_KEY" \
    -d '{"is_hourly": true, "hourly_quantity": 42}'

{
  "id": "inv_8f2c",
  "invoice_number": "BB-2026-0197",
  "is_hourly": true,
  "hourly_rate": 165,
  "hourly_quantity": 42,
  "amount": 6930,
  "gst": 693,
  "total": 7623
}
The guard reads the request body, not the record, so the caller has to state the billing mode before anything moves.
The recalculated figures reconcile
amount  =  42 hours × 165.00  =  6,930.00
gst  =  6,930.00 × 0.1  =  693.00  (the hourly path multiplies by 10% directly, because one engagement is one rate)
total  =  6,930.00 + 693.00  =  7,623.00

Read either way round, this is a decision about who is allowed to move money. A PATCH that re-derives a total from whichever fields it happens to touch lets any partial update rewrite an issued invoice as a side effect. Naming the billing mode makes the recalculation an instruction rather than an inference, which is what you want from the endpoint an automated agent is holding.

Endpoint Reference

GET /api/invoice?action=find_client

Fuzzy-matches a client by name, so a rough spelling resolves to the record. A name that matches nothing is refused rather than quietly creating a second client.

{ "found": true, "client": { "id": "cl_41a9", "name": "Ravensworth Civil Pty Ltd", "email": "[email protected]" } }
POST /api/invoice

Raises an invoice against a matched client name, with a number and a due date.

{ "id": "inv_8f2c", "invoice_number": "BB-2026-0197", "due_date": "2026-04-09", "amount": 0, "gst": 0, "total": 0, "status": "draft" }
POST /api/invoice/{id}/line-items

Adds a line and recomputes the parent invoice from every line. Two guards apply: an invoice with no lines keeps its stored totals, and lines that all sum to zero against a real recorded amount are treated as notes rather than zeroing it.

{ "item_name": "Reconciliation review", "quantity": 9, "unit_price": 165, "gst_percent": 10, "is_hourly": true }
PATCH /api/invoice/{id}

Updates permitted fields only. Setting status to paid stamps the payment date when one is not supplied, and moving the status off paid clears it again, so the pair cannot drift apart.

{ "status": "paid" }   ->   paid_date set to today automatically
DELETE /api/invoice/{id}

Soft-deletes through a deletion log, so a removal stays auditable and appears on the Deletion Log screen rather than a row simply disappearing.

{ "deleted": true, "logged_to": "deletion_log" }

Claude Code Setup

With the key and base URL exported, the ledger can be driven in a sentence. This is the reason the API exists: invoicing is the administrative work around the work, and it should cost a sentence rather than a form.

You
Bill Ravensworth 9 hours of reconciliation review at the usual rate, due 9 April.
Claude Code
Matched ravensworth to Ravensworth Civil Pty Ltd. Raised BB-2026-0197 and added the line at 9 × $165.00. Invoice totals recomputed from all 5 lines: subtotal $8,300.00, GST $830.00, total $9,130.00. Due 9 Apr 2026.

Brontiq Books is a real, shipped product and this API is live behind a bearer token. This screen is drawn from the product's own API documentation page. Every client, contact, email, invoice number, record id and figure on it is invented, the API key is the placeholder string the real page ships with, and no request shown here was sent to production. The $9,130.00 in the last exchange is the same total the invoice editor and ledger screens carry for BB-2026-0197.