* 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>
185 lines
7.4 KiB
Markdown
185 lines
7.4 KiB
Markdown
<!-- GENERATED FILE, do not edit. Source: lib/api/v1 registry + scripts/api-skill/overlays. Regenerate with `npm run apiskill:generate`. -->
|
|
|
|
# Core endpoints
|
|
|
|
Connectivity, company discovery, async-operation polling, and company settings. Every session starts with GET /companies to resolve the companyId that all other URLs need.
|
|
|
|
Conventions (auth, envelope, pagination, dry-run, idempotency, standard errors)
|
|
are in SKILL.md and are not repeated per endpoint.
|
|
|
|
### `GET /api/v1/companies`
|
|
|
|
**List companies the API key can access.**
|
|
`scope:companies:read · risk:low · idempotent`
|
|
|
|
Returns every non-archived company the API key user is a member of, together with their role. Use the returned `id` as `{companyId}` in subsequent endpoints.
|
|
|
|
**Use when:** You need to discover which company IDs an API key has access to before calling company-scoped endpoints.
|
|
**Do not use for:** Fetching a single company you already know the id of: use GET /api/v1/companies/{companyId} for that.
|
|
|
|
**Pitfalls:**
|
|
- Multi-company keys (e.g. consultants) will see >1 result. Always pass the correct companyId in subsequent paths.
|
|
- Archived companies are excluded; if a company disappears the user has been removed from it or it was archived.
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: { id: string, name: string, org_number: string, entity_type: string, role: "owner" | "admin" | "member" | "viewer", created_at: string }[],
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `PATCH /api/v1/companies/{companyId}/settings`
|
|
|
|
**Partially update company settings.**
|
|
`scope:companies:write · risk:medium · idempotent · dry-run · reversible`
|
|
|
|
Patches the company payment details (bank account, Bankgiro, Plusgiro, Swish, IBAN/BIC), the contact details shown on invoices (contact_person, email, phone, website), and the custom invoice email texts. All fields optional; at least one must be supplied. Idempotent (mandatory Idempotency-Key). Dry-runnable. The same validation as the MCP staging tool applies: Bankgiro/Plusgiro numbers are Luhn-checked and invoice email texts only accept a fixed placeholder set.
|
|
|
|
**Use when:** You need to change the payment or contact details that appear on invoices, or override the invoice email texts, directly over REST instead of the staged MCP flow.
|
|
**Do not use for:** Legal or tax profile changes (org number, VAT registration, fiscal year, accounting method): those are not exposed on the public API. Reading settings (no GET endpoint yet; use the MCP tool gnubok_get_company_settings).
|
|
|
|
**Pitfalls:**
|
|
- Idempotency-Key is mandatory; calls without it return 400.
|
|
- contact_person is stored as default_our_reference: the default "Our reference" value on new invoices.
|
|
- bankgiro and plusgiro must carry a valid Luhn check digit; null or empty string clears them.
|
|
- invoice_email_texts only accepts the placeholders {fakturanummer} {kundnamn} {förnamn} {företag} {förfallodatum} {belopp}; any other {token} is rejected. Null clears every override.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `companyId` | path | `string` | yes | |
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
bank_name?: string,
|
|
clearing_number: string | "",
|
|
account_number: string | "",
|
|
bankgiro: string | "",
|
|
plusgiro: string | "",
|
|
swish?: string,
|
|
iban: string | "",
|
|
bic: string | "",
|
|
contact_person?: string,
|
|
email: string | "",
|
|
phone?: string,
|
|
website: string | "",
|
|
invoice_email_texts?: {
|
|
sv?: { subject?: string, greeting?: string, body?: string, signoff?: string },
|
|
en?: { subject?: string, greeting?: string, body?: string, signoff?: string }
|
|
}
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
company_id: string,
|
|
bank_name: string,
|
|
clearing_number: string,
|
|
account_number: string,
|
|
bankgiro: string,
|
|
plusgiro: string,
|
|
swish: string,
|
|
iban: string,
|
|
bic: string,
|
|
contact_person: string,
|
|
email: string,
|
|
phone: string,
|
|
website: string,
|
|
invoice_email_texts: { sv?: { subject?: string, greeting?: string, body?: string, signoff?: string }, en?: { subject?: string, greeting?: string, body?: string, signoff?: string } }
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `GET /api/v1/health`
|
|
|
|
**Health check.**
|
|
`risk:low · idempotent`
|
|
|
|
Reports the API is reachable and what version is currently served. Public; no auth required.
|
|
|
|
**Use when:** You want to verify connectivity, latency, or which API version is live before issuing other requests.
|
|
**Do not use for:** Anything that needs authenticated data. This endpoint returns no company-specific information.
|
|
|
|
**Pitfalls:**
|
|
- A 200 here only means the API process responds: downstream Postgres/Supabase may still be degraded.
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: { status: "ok" | "degraded", service: "gnubok", api_version: string, timestamp: string },
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `GET /api/v1/operations/{id}`
|
|
|
|
**Poll a long-running operation by id.**
|
|
`scope:operations:read · risk:low · idempotent`
|
|
|
|
Returns the current snapshot of a v1 async operation: status (queued / running / succeeded / failed / cancelled), progress (jsonb, free-form), result (on success), and error (on failure). The operation_id is returned by the POST endpoints that initiate async work (period close, year-end, currency revaluation, SIE import).
|
|
|
|
**Use when:** You started an async operation and need to know whether it has finished. Poll every 5-30 seconds until a terminal status. (The 202 response advertises `operation.completed` as the eventual push signal, but that webhook event is not deliverable yet — polling is the only supported completion signal today.)
|
|
**Do not use for:** Fetching the resource the operation produced: once status=succeeded, read the result field or call the resource-specific GET endpoint. Cancelling a running operation (no cancel endpoint exists in v1).
|
|
|
|
**Pitfalls:**
|
|
- Terminal statuses (`succeeded`, `failed`, `cancelled`) are final; the row never transitions out of them.
|
|
- progress is free-form jsonb; agents should treat it as opaque except for the documented fields `phase` (string), `current` / `total` (numbers for percent calculation).
|
|
- started_at is null while status=queued (the work has not begun yet); completed_at is null until a terminal status is reached.
|
|
|
|
| Parameter | In | Type | Required | Notes |
|
|
|---|---|---|---|---|
|
|
| `id` | path | `string` | yes | |
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
operation_id: string,
|
|
type: string,
|
|
status: "queued" | "running" | "succeeded" | "failed" | "cancelled",
|
|
progress?: Record<string, unknown>,
|
|
result: unknown,
|
|
error: { code?: string, message?: string, details?: unknown },
|
|
started_at: string,
|
|
completed_at: string,
|
|
poll_url: string,
|
|
webhook_event: "operation.completed"
|
|
},
|
|
meta: {
|
|
request_id: string,
|
|
api_version: string,
|
|
next_cursor?: string,
|
|
audit?: { voucher_number?: string, voucher_url?: string, audit_trail_url?: string, immutable_at?: string },
|
|
partial_expansions?: string[]
|
|
}
|
|
}
|
|
```
|