Files
accounted/scripts/api-skill/overlays/conventions.md
T
Jakob Wennberg 11b82cbb91 feat(api): installable accounted-api agent skill + openapi-to-skill generator (#1516)
* feat(api): installable accounted-api agent skill + openapi-to-skill generator

Three layers, per the July/August 2026 agent-skills ecosystem (skills.sh /
npx skills add, as used by Stripe/Cloudflare/Supabase for their APIs):

- skills/openapi-to-skill/: generic, installable skill that turns any
  OpenAPI spec into a consumer-side integration skill, with a portable
  stdlib-only inventory/condenser tool and an output template + quality
  checklist encoding the distill-not-restate methodology.
- skills/accounted-api/: the installable skill for our own API, rendered
  deterministically by scripts/api-skill/generate.ts from the v1 endpoint
  registry + hand-authored overlays (auth, conventions, domain gotchas).
  CI gate: npm run apiskill:check (core-build.yml).
- lib/api/v1/registry.ts: generateOpenApiSpec now emits requestBody (incl.
  multipart binary parts) and path parameters, and the Zod converter learned
  .default()/z.record()/.pipe()/.transform(), so the public spec carries
  request contracts instead of prose-only.

Docs: /docs/api landing + /llms.txt now point agents at the skill install;
corrected the stale test-key description in the landing (test keys read
real data and force dry-run writes; they are not sandbox-company bound).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): escape backslashes in markdown table cells (CodeQL js/incomplete-sanitization)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 12:45:19 +02:00

3.9 KiB

Conventions

These rules hold across the whole surface; endpoint entries below do not repeat them.

Response envelope. Success: { "data": ..., "meta": { "request_id", "api_version", "next_cursor"?, "audit"?, "partial_expansions"? } }. Errors replace data with error (no meta; request_id moves inside error).

Errors. Stable machine codes with agent-oriented remediation:

{ "error": { "code": "PERIOD_LOCKED", "message": "Svenska", "message_en": "English",
  "details": {}, "recovery_hint": "next step", "docs_url": "...",
  "valid_alternatives": {}, "request_id": "req_..." } }

React to code, read message_en and recovery_hint, follow valid_alternatives when present (e.g. next_open_period). Standard codes on every endpoint: 400 validation, 401 bad key, 403 missing scope, 404, 429 rate limited (honor Retry-After), 500. Full catalogue: https://app.gnubok.se/docs/api/errors. Only endpoint-specific codes are mentioned per endpoint below.

Cursor pagination. List endpoints take ?cursor= and return meta.next_cursor; loop until it is absent/null. A stale or tampered cursor is NOT an error: the first page is returned again, so terminate on next_cursor, never on "page looks familiar".

Dry-run on every write that supports it (dry-run badge in the index). Send ?dry_run=true (or X-Dry-Run: true): the response is always 200 with data.dry_run: true plus a preview (would-be record, journal lines, voucher number) and the X-Dry-Run: true response header; nothing is committed. Commit by re-issuing without the flag and with the SAME Idempotency-Key (the dry run is not cached against the key). Preview first on any financial write; it is free.

Idempotency-Key. Send a fresh UUID header on every POST/PATCH/DELETE; several endpoints reject writes without one (400). Replaying the same key+body returns the original response with the Idempotent-Replayed: true header; the same key with a different body returns 409 IDEMPOTENCY_KEY_REUSE (24h window). Safe retry loop: keep the key, keep the body.

Test keys are simulation-only. With a gnubok_sk_test_* key, reads return real company data (responses carry X-Gnubok-Mode: test) and every write is forced into dry-run; writes that cannot be simulated return 403 TEST_KEY_WRITE_BLOCKED. Nothing a test key does ever persists: it is ?dry_run=true baked into the credential. Full end-to-end write tests therefore need a live key against a company you own.

Atomic writes. A mutation either commits fully or errors with no side effects. There is no partial state to clean up after an error response (bulk-create endpoints that do partial success say so explicitly).

Audit inline. Successful financial writes include meta.audit (voucher number, audit-trail URL, immutability timestamp). No follow-up read needed to confirm what was booked.

Expansion. Some list/detail endpoints take ?expand=a,b (documented per endpoint). If an expansion fails the response still succeeds and names the failed parts in meta.partial_expansions; check it before trusting expanded fields.

Async operations. Long-running actions (fiscal-period lock/close/year-end, imports) return 202 with an operation id; poll GET /api/v1/operations/{id} until status is succeeded/failed. The response shape is identical whether the work ran inline or queued.

Versioning. Dated versions (current: see meta.api_version). Pin with the Gnubok-Version request header; responses echo it. Additive changes ship without a version bump; see https://app.gnubok.se/docs/api/versioning.

Index badges. Every operation line below carries machine-readable annotations from the spec: scope: (required key scope), risk: (low/medium/ high; confirm with a human before unprompted high-risk calls), idempotent (safe to retry), dry-run (previewable), reversible (a single follow-up call can undo it, e.g. invoice credit).