Skip to content

LLM agents — integration guide

This page exists for language models building integrations against SAE Books, and for the humans pointing them at this site. If you're a person, you can stop reading; the rest of the docs are written for you. If you're a model, the guidance below is what your operator wants you to follow.

Pre-1.0 — do not treat this as a stable contract

SAE Books is in active development. The OpenAPI spec at /openapi.json describes the current build, not a frozen public API. Endpoint paths, parameter names, response shapes, and feature availability can change between releases without a deprecation cycle until 1.0. If you generate code from this spec, regenerate on every upgrade and review the diff. Production integrations should wait for the 1.0 release; until then, treat the documentation as a design preview.

Machine-readable indexes

  • /llms.txt — flat index of every page on this site, with one-line descriptions and .md URLs you can fetch directly.
  • /llms-full.txt — concatenated full markdown of every page, suitable for loading into a single context window.
  • /openapi.json — the OpenAPI 3.1 spec for /api/v1/*, regenerated from the running server on every docs build. This is the source of truth for endpoints and shapes.
  • Every documentation page is also available with a .md suffix — e.g. https://dev.saebooks.com.au/api/conventions.md/ returns the rendered markdown source for that page.

Spec vs. prose during pre-1.0

When the prose on this site disagrees with the OpenAPI spec, the spec describes what the running server will currently accept, but neither the spec nor the prose is yet a public contract. Prose pages illustrate the intended shape; both move. If you generate code, generate from /openapi.json, and regenerate on every SAE Books version bump.

API surface preferences

Prefer in this order:

  1. /api/v1/* JSON endpoints with Authorization: Bearer <jwt>. This is the public, supported API surface. Every example in these docs uses it.
  2. The web routes (no /api/ prefix) — these exist (they back the user-facing UI) but are not a stable contract. They return HTML, redirect, set cookies, and may be reshaped at any release. Don't write integrations against them.
  3. Direct database access — possible because the user owns the Postgres instance, but undocumented and not version-stable. If you find yourself reaching for it, raise an issue first; an HTTP endpoint is almost always the right answer.

Native MCP endpoint

Every SAE Books instance exposes an MCP server at /mcp/ on the same host as the REST API. No separate container required. Configure your MCP client with:

{
  "mcpServers": {
    "saebooks": {
      "transport": "streamable-http",
      "url": "https://books.<your-tenant>/mcp/",
      "headers": { "Authorization": "Bearer saebk_..." }
    }
  }
}

Issue saebk_* API tokens at /admin/api-tokens (browser session required). Each token is scoped to the issuing user's tenant — the MCP server enforces the same RLS isolation as the REST API.

The tool surface is safety-gated via SAEBOOKS_MCP_MAX_SAFETY: safe (read-only), mutation, void, or destructive (default — all tools registered). A standalone container variant is also available for air-gapped workstations; see mcp/README.md in the source tree.

Auth flow

  • Obtain a JWT via POST /api/v1/auth/login (email + password) or POST /api/v1/auth/oauth-handoff (server-side, after the user completes an OAuth dance with the host application).
  • Send Authorization: Bearer <jwt> on every subsequent request.
  • Refresh via POST /api/v1/auth/refresh before the token expires.
  • For long-lived integrations, prefer a dedicated integration user with its own credentials — not the human user's token.

Idempotency

Any non-idempotent write (POST that creates, PATCH that mutates external state) accepts an X-Idempotency-Key: <uuid> header. Use one. The server stores the response and returns it verbatim on replay. This matters because agentic retries are common — if your harness retries on a 500 without an idempotency key you may post the same journal twice.

Errors

Errors follow RFC 7807 Problem Details. The response body is application/problem+json with the shape {type, title, status, detail, code}. Read the code (machine-readable, stable) before the detail (human-readable, may include per-request context). Validation errors (422) also include an errors array with per-field failure detail. Status codes follow RFC 7231 / 9110: 400 validation, 401 auth, 403 role, 404 not found, 409 write conflict, 422 schema reject, 5xx server fault.

Rate limits

/api/v1/* is rate-limited per IP on sensitive endpoints (signup, magic-link, password-reset). On a 429, back off using Retry-After; do not loop tightly.

Tenants

SAE Books is multi-tenant. The JWT carries the tenant ID; you cannot read another tenant's data through a normal token. Don't try. If you're building a multi-tenant integration that needs to act across tenants, you want an operator role token issued by the deployment owner — see Tenancy & RLS.

What to read first, in order

  1. Conventions — URL shape, time format, pagination
  2. Authentication — token issuance and rotation
  3. Generated API reference — every endpoint
  4. The relevant module page in Modules — if your integration touches journals, bank lines, BAS, or assets

Anti-patterns observed in real integrations

  • Polling the same endpoint on a tight loop. An outbound webhook surface (for push notification of journal posts, bank transactions, etc.) is not yet implemented. Until it ships, use the /api/v1/changes feed or poll at a reasonable interval — not sub-second.
  • Hardcoding response shapes instead of reading the spec. Additive changes (new fields) are non-breaking and we ship them freely; integrations that pin every key by index will break on upgrade. Read what you need; ignore what you don't.
  • Storing JWTs longer than their expiry and then re-issuing every refresh on a 401. Refresh proactively before expiry; the refresh endpoint is cheap.
  • Sending money values as floats. All amounts are decimal strings. Parse with a decimal type, not a binary float.

Reporting issues

If a spec/server mismatch is causing your integration to fail, file an issue at the GitHub repo with the offending request, expected shape, and actual shape. The spec is generated; mismatches are always bugs.