Invora API, as shipped in the download
Invora REST API, Webhooks & MCP (3.2)
Invora ships a JSON REST API, signed webhooks and — new in 3.0 — an MCP endpoint, so your invoicing plugs into Power Automate, Zapier, n8n, AI agents, or your own code — including Microsoft Business Central integrations built against it.
Authentication
Create a key in Settings → API & Webhooks (admin). A key belongs to a user and can do exactly what that user can do in the app — clerks create documents, record payments and manage clients; admins additionally run the install. Send it on every call:
Authorization: Bearer apk_...
The key is shown once at creation and stored hashed. Revoke any time.
Endpoints
| Method & path | What it does |
|---|---|
GET /api/ping | Verify a key |
GET /api/invoices · GET /api/estimates | Newest 50; ?status= filters (draft, sent, partial, paid, overdue, accepted, declined) |
POST /api/invoices · POST /api/estimates | Create (client_id + lines[{description, qty, unit, tax_rate}]; optional status/discount/dates) |
GET /api/documents/{id} | One document with lines, payments, totals, public link |
POST /api/documents/{id}/payments | Record a payment (invoices only): {"amount": "125.00", "paid_on"?, "method"?} |
POST /api/documents/{id}/status | Set workflow status (draft/sent; estimates also accepted/declined) |
GET/POST /api/clients | List / create clients |
GET /api/items · GET /api/taxes | Catalog reference data |
GET /api/reminders | The automatic payment-reminder schedule, read-only (new in 3.2; see below) |
GET /api/openapi.json | OpenAPI 3 spec (no auth) |
Create an invoice
curl -X POST https://your-install/api/invoices \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"client_id": 1, "status": "sent",
"lines": [{"description": "Consulting", "qty": "3", "unit": "150.00", "tax_rate": "20"}]}'
Money is decimal-in, integer-cents-out. The exact same validation and totals math as the UI applies.
Webhooks
Add receiver URLs in Settings → API & Webhooks. Events:
invoice.created/estimate.created— a document is created: in the browser, by converting an estimate, through the API, by an AI tool or by a recurring schedulepayment.recorded— any payment landsinvoice.paid— the balance reaches zeroestimate.accepted— marked accepted, or converted to an invoice
Each delivery is an HTTP POST with a JSON body ({"event", "at", "document": {...}}) and:
X-Invora-Event: invoice.created
X-Invora-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>
Verify the signature before trusting a payload. Delivery is one attempt with a 5-second timeout — deliberately simple; the Recent deliveries log on the settings page shows every attempt and response code. Design receivers to be idempotent.
Power Automate (and Logic Apps)
Two ways, no Microsoft partnership required:
- Instant trigger via webhook: in Power Automate, create a flow with the "When an HTTP request is received" trigger, copy its URL into Invora's webhooks, and branch on
body('...')?['event']. That's your invoices driving Teams messages, Business Central jobs, or anything else in your tenant. - Custom connector: Power Automate → Data → Custom connectors → **Import an OpenAPI file** → point it at
/api/openapi.jsonfrom your install (or download the file). Set security to API Key / Bearer. Every endpoint above becomes a native action.
Limits, honestly
- The API acts at user level; there is no separate scope system beyond the app's roles.
- One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
- List endpoints cap at 50–100 rows; this is an SMB invoicing tool, not a data warehouse.
MCP — the agent door (new in 3.0)
Invora speaks the Model Context Protocol at POST /mcp, so Claude, ChatGPT agents, n8n's AI nodes or your own code can use Invora instead of merely reading it. It is the same product underneath: the same bearer key, the same roles, and the same money math. An agent cannot do anything its key's user could not do in the browser — and it cannot compute a total, ever; Invora's integer-cent engine does.
Transport is streamable HTTP, stateless: one JSON-RPC 2.0 request in, one JSON response out.
curl -s -X POST https://invora.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1"}}}'
Then tools/list to discover, tools/call to act:
curl -s -X POST https://invora.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"overdue_report","arguments":{}}}'
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://invora.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http invora https://invora.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "invora",
"server_url": "https://invora.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "invora", "name": "Invora", "url": "https://invora.example.com/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://invora.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Invora implements the protocol, not an integration with a particular vendor.
Tools
| Tool | Writes? | What it does |
|---|---|---|
list_invoices | no | Invoices newest first (max 50); optional effective-status filter |
list_estimates | no | Estimates newest first (max 50); optional status filter |
get_document | no | One document with lines, payments and totals |
create_invoice | yes | Creates an invoice from client_id + lines; totals computed by Invora |
create_estimate | yes | Creates an estimate, same shape |
record_payment | yes | Records a payment; a fully-paid invoice fires the paid webhook |
set_status | yes | draft/sent (estimates also accepted/declined) — paid/partial/overdue are computed, never set |
overdue_report | no | Counts by effective status + the overdue invoices with balances |
list_clients | no | Clients on file |
reminder_schedule | no | The automatic payment-reminder schedule: steps, and per open invoice the last step sent, today, and the next one (new in 3.2) |
There is deliberately no send-to-client tool and no delete tool. A customer is emailed only by a person in the app or by the reminder schedule the owner switched on in Settings, and records are destroyed only by a person. An agent can draft your recurring work, tell you what is overdue and read the reminder schedule; it cannot write to your customers, trigger or pause a reminder, or erase your books.
Money discipline
Send decimals as strings ("unit": "150.00", "amount": "459.99"). Invora parses them with the same to_cents/to_milli guards the form uses and computes every total itself. A nested value where a scalar belongs, an unknown argument, or an unknown client are each refused by name with isError: true — the same sentences a person sees.
Operational endpoints (new in 3.0)
| Endpoint | Guard | What it does |
|---|---|---|
GET /backup/scheduled?t=<token> | ops token (Security page) | Writes a redacted JSON backup into data/backups/, keeps the newest 14 |
GET /cron/recurring?t=<token> | same ops token | Drafts every recurring invoice that is due (never sends) |
GET /cron/reminders?t=<token> | same ops token | Sends the automatic payment reminders due today, up to the daily cap; does nothing while they are switched off (new in 3.2) |
GET /calendar.ics?t=<token> | its own feed token | Read-only iCal feed of open invoices' due dates |
GET /clients/{id}/gdpr.json | admin session | Subject-access export for one client |
Automatic payment reminders (new in 3.2): see, never send
Reminders are off by default. The owner switches them on in **Settings → Automatic payment reminders**, approving the steps (days from the due date, -3,0,7,14 by default), a daily cap and one message per step. They then go out on normal page loads, or from /cron/reminders for an install nobody visits, through the owner's own SMTP server. Every attempt is recorded in the invoice's send log with its step.
The API and the MCP door can read the schedule and nothing more. GET /api/reminders and the MCP tool reminder_schedule return the same JSON:
{
"enabled": true, "armed": true, "not_armed_because": [], "today": "2026-09-24",
"steps": [{"days": -3, "label": "3 days before the due date"}, {"days": 0, "label": "on the due date"},
{"days": 7, "label": "7 days after the due date"}, {"days": 14, "label": "14 days after the due date"}],
"daily_cap": 20, "attempts_today": 2,
"invoices": [
{"id": 12, "number": "INV-0012", "client_id": 3, "client": "Aria Okafor", "status": "overdue",
"due_date": "2026-09-17", "balance_cents": 47740,
"last_step": {"days": 0, "label": "on the due date", "on": "2026-09-17", "status": "sent"},
"today": {"state": "send", "step": 7, "reason": ""},
"next": {"days": 7, "label": "7 days after the due date", "on": "2026-09-24"}}
],
"note": "Read-only. Reminders go only on the schedule the owner switched on in Settings; nothing here can send one."
}
today.state is one of send (goes today), would_send (would go today, but the schedule is not armed: see not_armed_because), held (due today but held: paused, no address, the day's cap, a refused send, or a quiet day after another mail; reason says which), done (today's step already went), waiting (no step has come yet) or finished. The response carries no email addresses and no message text. There is no route or tool that sends, pauses or edits a reminder.