A REST surface over your own ledger, plus signed notifications the moment records change. What follows describes the interface while it is still being finished, so read the status note before committing a roadmap to it.
Status: written, working, and under test — but not yet enabled in a shipped release. We are publishing early precisely so integrators can push back while changing the design is still cheap. To hear the day it goes live, use the request form on the Integrations page.
Authentication is one API key presented as a bearer token — no OAuth dance, no refresh cycle. Keys are minted from within Kantivo under Settings → Integrations → API Keys by an admin or manager.
curl https://your-install.local:3000/api/customers \
-H "Authorization: Bearer kv_live_7fa39c21e4b85d06f1c2a930"
Where bearer headers are inconvenient in your HTTP client, an X-API-Key header does the same job.
A key is bound to one company. Multi-company installs issue a separate key per company, and the key decides which books it touches. There is no company header — sending one that disagrees with the key returns 403.
The full key is shown exactly once, at creation. We store only a SHA-256 hash plus a short display prefix, so we cannot recover it for you. Lost it? Revoke that key and issue another.
Because Kantivo is desktop software, the base URL is your own installation, not a service we host. On the same machine that is http://localhost:3000. Reaching it from elsewhere on your network is a decision you make deliberately — see the security note at the end.
Every key carries one or more scopes. Issue the least privileged one that still gets the work done.
| Scope | Grants |
|---|---|
read | GET on every supported resource. |
write | Everything read allows, plus POST and PUT. |
A key without the required scope gets 403 with a message naming the scope it needed.
Keys reach only what is enumerated below. This is an allowlist rather than a filter: routes existing elsewhere in the application stay unreachable to a key until we deliberately publish them, so the public surface cannot widen by accident.
| Method | Path | Scope |
|---|---|---|
| GET | /api/customers | read |
| GET | /api/customers/:id | read |
| POST | /api/customers | write |
| PUT | /api/customers/:id | write |
| GET | /api/vendors | read |
| POST | /api/vendors | write |
| GET | /api/invoices | read |
| GET | /api/invoices/:id | read |
| POST | /api/invoices | write |
| PUT | /api/invoices/:id | write |
| GET | /api/bills | read |
| POST | /api/bills | write |
| GET | /api/accounts | read |
| GET | /api/items | read |
| GET | /api/estimates | read |
| GET | /api/transactions | read |
| POST | /api/transactions | write |
curl -X POST http://localhost:3000/api/customers \
-H "Authorization: Bearer kv_live_..." \
-H "Content-Type: application/json" \
-d '{
"customer_name": "Riverside Dental",
"email": "ap@riversidedental.example",
"phone": "555-0142",
"city": "Boise",
"state": "ID"
}'
An invoice raised through the API posts to the ledger identically to one typed into the app, including tax and multi-currency handling. A draft invoice does not post until it is issued.
curl -X POST http://localhost:3000/api/invoices \
-H "Authorization: Bearer kv_live_..." \
-H "Content-Type: application/json" \
-d '{
"customer_id": 42,
"invoice_date": "2026-08-04",
"due_date": "2026-09-03",
"status": "sent",
"items": [
{ "description": "Consulting - August", "quantity": 12, "unit_price": 145.00 }
]
}'
customer_name, invoice_date, total_amount.YYYY-MM-DD. Timestamps come back as ISO 8601 in UTC.145.00, never 14500.Content-Type: application/json on anything with a body.Errors use standard status codes with a JSON body carrying an error string written for a human reading a log.
| Status | Meaning |
|---|---|
400 | The request was malformed or failed validation. |
401 | Key missing, invalid, revoked or expired. |
403 | Key is valid but lacks the scope, or the endpoint is not on the allowlist. |
404 | No such record in this company. |
429 | Rate limited. Back off and retry. |
500 | Our fault. Safe to retry an idempotent request. |
The default rate limit is 100 requests per 15 minutes per IP, matching the rest of the application.
Skip the polling loop — register a URL and Kantivo posts to it as events occur. Endpoints are managed under Settings → Integrations → Webhooks.
| Event | Fires when |
|---|---|
invoice.created | An invoice is created, by any route. |
invoice.updated | An existing invoice is modified. |
invoice.paid | An invoice is settled in full. |
customer.created | A customer record is added. |
customer.updated | A customer record changes. |
vendor.created | A vendor record is added. |
bill.created | A bill is entered. |
payment.received | A customer payment is recorded. |
transaction.created | A journal entry is posted. |
Subscribe to * to receive everything, including events added later.
POST /your-endpoint
X-Kantivo-Event: invoice.created
X-Kantivo-Event-Id: evt_9c1f04ab77e2
X-Kantivo-Timestamp: 1786000000
X-Kantivo-Signature: 4f1c...<64 hex chars>
{
"id": "evt_9c1f04ab77e2",
"type": "invoice.created",
"created_at": "2026-08-04T15:12:09.441Z",
"data": {
"id": 1180,
"invoice_number": "INV-1042",
"customer_id": 42,
"total_amount": "1740.00",
"status": "sent"
}
}
Deliveries carry an HMAC computed from the secret shown at registration. Check it before acting on anything: unverified, whoever discovers your URL can feed your systems invented financial events.
Sign the string {timestamp}.{raw body} with HMAC-SHA256 and compare to the header:
const crypto = require('crypto');
function verify(req, rawBody, secret) {
const timestamp = req.headers['x-kantivo-timestamp'];
const signature = req.headers['x-kantivo-signature'];
// Reject anything older than five minutes to blunt replay attacks.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'utf8'),
Buffer.from(expected, 'utf8')
);
}
Sign the raw body, not a re-serialized object. Parsing the JSON and stringifying it again reorders keys and changes whitespace, and the signature will never match. Capture the body as a string first.
X-Kantivo-Event-Id for idempotency. A retry reuses the same id, and a machine that was asleep may receive a burst at once. Treat a repeated id as already handled.https://.Both connectors sit on exactly what is documented here. Planned triggers: invoice raised, invoice settled, customer added, bill entered. Planned actions: raise an invoice, add a customer, record a payment. Nothing stops you building the equivalent yourself today — the connectors get no privileged access.
Kantivo runs on your hardware, so an API key is only useful to something that can reach your machine. That is a genuine security advantage — there is no public endpoint for an attacker to find. It also means that if you want an outside service to call in, opening that path is your decision, and you should treat an API key with the same care as the login it acts on behalf of.
Spotted a gap, or need something this design cannot express? Get in touch. The shape is still soft, which is when an integrator opinion counts for most.