v0.1.0 Latest

REST API and webhooks

/api/v1 with bearer keys and an OpenAPI spec, company scoping, PSA lookups, deep links, signed webhooks.

Base: /api/v1. JSON. Auth: Authorization: Bearer <api_key>; the key is also accepted bare, or as X-API-Key. The spec is generated from the same Zod schemas the endpoints validate with and served at /api/v1/openapi.json. Cursor pagination: ?limit=50&cursor=..., response carries next_cursor.

Keys are created under Admin → API keys with scopes read, write, admin, reactions, and with either every company or a named set. The value is shown once.

Resources

GET    /companies                  ?q=&external_system=&external_id=
POST   /companies
GET    /companies/:id
PATCH  /companies/:id
GET    /companies/:id/locations
GET    /companies/:id/documents    ?doc_type=&location_id=
GET    /companies/:id/export       JSON or Markdown

POST   /locations
GET    /locations/:id
PATCH  /locations/:id

GET    /doc-types                  (includes template fields)
GET    /doc-types/:id

GET    /documents/:id              (resolved values + raw IDs)
POST   /documents                  { company_id, doc_type_id, location_id?, title, field_values }
PATCH  /documents/:id              (partial field_values merge)
GET    /documents/:id/revisions

GET    /option-lists/:id/items
POST   /option-lists/:id/items

GET    /search                     ?q=&company_id=&doc_type=
GET    /users
POST   /users                      { email, name, role?, all_companies? }

field_values is keyed by field UUID, exactly as stored. Responses return resolved values (option labels, linked document titles, rendered markdown) alongside the raw ids.

Company scope

A key is created with either every company or a named set. Everything is filtered by it: lists omit what the key may not see, and a single record it may not see answers 404 not_found, the same answer a missing id gets, so a key cannot be used to find out which companies exist. POST /companies with a restricted key answers 403 forbidden, since the key could not see what it created.

PSA integration

A ticket in any PSA needs to show that client’s docs:

PUT    /external-refs              { entity, entity_id, system, external_id }  (upsert)
GET    /lookup                     ?system=halopsa&entity=company&external_id=123
                                   -> the company plus its locations and documents summary

Deep links a PSA can embed without the API: /go/{system}/company/{external_id} redirects to the matching company page.

Vault

Requires the secrets:reveal scope, off by default.

GET    /vault/items                ?company_id=&q=      (metadata only, scoped to the mapped collection)
POST   /vault/items/:item_id/reveal   { document_id, field_id }  -> { password }  (audited, no-store)
POST   /vault/items/:item_id/totp     { document_id, field_id }  -> { code, period_remaining }

Secrets never appear in document, revision, search, export, or webhook payloads.

Knowledge base

See Knowledge base for the /kb/* routes: collections, search, articles with runbook steps, upsert and archive under an external id, grants, and favorites and votes for a named reader.

Webhooks

Created under Admin → Notifications. Events:

  • company.created, company.updated
  • location.created, location.updated
  • document.created, document.updated, document.archived
  • field.promoted
  • document.due
  • kb.article.upserted, kb.article.archived ({ collection_id, article_id, external_id, kind })

Payload:

{ "event": "document.updated", "occurred_at": "...", "data": { "...": "resolved document" } }

Headers: X-Trove-Event, X-Trove-Delivery, X-Trove-Signature: sha256=<hmac> over the raw body with the signing secret shown once when the webhook was made. Retries with exponential backoff, up to 8 attempts, from a worker in the app container. On a Workers deployment a Cron Trigger calls /api/internal/webhooks with CRON_SECRET for the same pass.

Security baseline

  • Argon2id for local passwords
  • API keys: random 32 bytes, shown once, stored as SHA-256, looked up by prefix
  • Server-side HTML sanitization for rich text
  • CSRF protection on session routes, rate limiting on auth and the API