◷ In Development

Building on Kantivo

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.

On this page

Authenticating

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.

Scopes

Every key carries one or more scopes. Issue the least privileged one that still gets the work done.

ScopeGrants
readGET on every supported resource.
writeEverything read allows, plus POST and PUT.

A key without the required scope gets 403 with a message naming the scope it needed.

What a key can reach

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.

MethodPathScope
GET/api/customersread
GET/api/customers/:idread
POST/api/customerswrite
PUT/api/customers/:idwrite
GET/api/vendorsread
POST/api/vendorswrite
GET/api/invoicesread
GET/api/invoices/:idread
POST/api/invoiceswrite
PUT/api/invoices/:idwrite
GET/api/billsread
POST/api/billswrite
GET/api/accountsread
GET/api/itemsread
GET/api/estimatesread
GET/api/transactionsread
POST/api/transactionswrite

Creating a customer

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"
  }'

Creating an invoice

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 }
    ]
  }'

Payload conventions

Error semantics

Errors use standard status codes with a JSON body carrying an error string written for a human reading a log.

StatusMeaning
400The request was malformed or failed validation.
401Key missing, invalid, revoked or expired.
403Key is valid but lacks the scope, or the endpoint is not on the allowlist.
404No such record in this company.
429Rate limited. Back off and retry.
500Our 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.

Webhooks

Skip the polling loop — register a URL and Kantivo posts to it as events occur. Endpoints are managed under Settings → Integrations → Webhooks.

EventFires when
invoice.createdAn invoice is created, by any route.
invoice.updatedAn existing invoice is modified.
invoice.paidAn invoice is settled in full.
customer.createdA customer record is added.
customer.updatedA customer record changes.
vendor.createdA vendor record is added.
bill.createdA bill is entered.
payment.receivedA customer payment is recorded.
transaction.createdA 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"
  }
}

Checking the signature

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.

How delivery behaves

Zapier & Make

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.

On network exposure

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.