* feat(onboarding): company setup from the conversation and POST /api/v1/companies Third PR of agent-first onboarding (#1814). Once connected, the agent can now set up a company end to end without the web wizard, and partner platforms can provision companies over REST. - create_company_for_user: service-role-only SECURITY DEFINER twin of create_company_with_owner taking the owner explicitly (service clients have no auth.uid()). pg-real test covers creation, role gating, unknown owner and foreign team. - lib/company/create-company.ts: the wizard's creation sequence (org number, TIC snapshot, BAS chart, settings, first fiscal period, tax deadlines, rollback) extracted into createCompanyCore; the Server Action delegates to it, behaviour unchanged. - lib/company/onboarding-input.ts: one Zod schema + planner for the agent/API paths; a VAT-registered company without moms_period is refused (a missing period silently yields zero VAT deadlines). - MCP: gnubok_create_company (two-phase: preview, then confirm=true; companies:write, company-independent), gnubok_connect_bank and gnubok_connect_skatteverket (status + the browser link, gated on bank_sync / skatteverket, search-only in the catalog), the "onboarding" skill, and initialize instructions pointing at it. - Consent page pre-ticks companies:write for an account with no company yet, so the setup does not dead-end on insufficient scope after signup. - POST /api/v1/companies (companies:write, dry-run aware) on the same core; scope map, registry, spec snapshot and the generated API skill updated. - tools/list payload ceiling raised 59.95K -> 60.4K for the one new default-catalog tool (documented in the guard). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(onboarding): explicit f_skatt, org number when VAT-registered, EF first year ends 31 Dec Review findings on #1864 (Swedish compliance review): - f_skatt is required, never defaulted to approved (SE-R-005 risk). - org_number is required when vat_registered: the invoice momsregistreringsnummer derives from it (ML 17 kap 24 §). - An enskild firma's first fiscal year must end on 31 December and its start month is forced to 1 even with first_fiscal_year set, mirroring the wizard's own rule text (BFL 3 kap. 1 §). - POST /api/v1/companies no longer claims Idempotency-Key support (the wrapper only honours it on company-scoped routes). - pg-real: createCompanyCore's chart seed runs under the real service_role, which the unit tests could not prove. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * test(pg): starter chart has 41 accounts, assert non-empty The service_role chart-seed proof passed the part that mattered (no 42501 from seed_chart_of_accounts) and failed on a wrong row-count guess: the seeded chart is a curated starter set, not the full BAS list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(migrations): move create_company_for_user to 20260825120000 main gained 20260824170000_bulk_book_transactions_service_actor.sql with the same version while this branch was open; two files on one version abort every Supabase branch apply and the prod auto-apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * chore(api): refresh spec snapshot and generated skill after rebasing onto main Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(mcp): flat create_company result, refuse localhost connect links, test hygiene CodeRabbit on #1864: the confirmed-create result was wrapped in the { data, next } envelope while its outputSchema promised top-level fields; it now returns the fields with next as a sibling. The two connect-link tools refuse to build a link when NEXT_PUBLIC_APP_URL is unset instead of handing a remote user a localhost URL. Tests clear mocks and the event bus in beforeEach. Not changed: the rollback already survives user_preferences.active_company_id (that FK is ON DELETE SET NULL since 20260331010000), and v1 error details stay in the surface's English developer convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
248 lines
10 KiB
Markdown
248 lines
10 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[]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `POST /api/v1/companies`
|
|
|
|
**Create a company and set it up for bookkeeping.**
|
|
`scope:companies:write · risk:medium · dry-run`
|
|
|
|
Creates a new company owned by the API key user (or attached to one of their teams) and sets it up in one call: owner membership, BAS chart of accounts for the company form, compliance settings, the first fiscal period and the automatic tax deadlines. A 30-day trial with every paid capability starts immediately. Intended for partner platforms provisioning client companies (byrå/vertical SaaS) and for agents onboarding a user.
|
|
|
|
**Use when:** A platform or agent needs to provision a company that does not exist in Accounted yet. The caller becomes its owner; invite the end customer afterwards.
|
|
**Do not use for:** Companies that already exist (list them with GET /api/v1/companies), or changing settings on an existing company (PATCH /api/v1/companies/{companyId}/settings).
|
|
|
|
**Pitfalls:**
|
|
- A VAT-registered company MUST send moms_period (monthly / quarterly / yearly); the request is refused otherwise, because a missing period silently produces zero VAT deadlines.
|
|
- Bookkeeping duty under BFL starts when the company exists with a fiscal period: do not create companies to try things out. Use a test-mode key (dry run) for that.
|
|
- Enskild firma always runs on the calendar year; fiscal_year_start_month is ignored for it.
|
|
- first_fiscal_year is only for a company in its first year (BFL 3 kap.: up to 18 months). Omit it for an established company.
|
|
- Not idempotent, and Idempotency-Key is not honoured on this company-less route: a retry after a network failure creates a second company. List GET /api/v1/companies before retrying.
|
|
- org_number is required for a VAT-registered company (the invoice momsregistreringsnummer derives from it), and f_skatt must be stated explicitly: F-skatt approval is never assumed.
|
|
|
|
Request body:
|
|
```ts
|
|
{
|
|
name: string,
|
|
entity_type: "enskild_firma" | "aktiebolag",
|
|
org_number?: string,
|
|
vat_registered: boolean,
|
|
moms_period?: "monthly" | "quarterly" | "yearly",
|
|
accounting_method: "accrual" | "cash",
|
|
f_skatt: boolean,
|
|
fiscal_year_start_month?: number,
|
|
first_fiscal_year?: { start: string, end: string },
|
|
address_line1?: string,
|
|
postal_code?: string,
|
|
city?: string,
|
|
team_id?: string
|
|
}
|
|
```
|
|
|
|
Response `200`:
|
|
```ts
|
|
{
|
|
data: {
|
|
id: string,
|
|
name: string,
|
|
entity_type: "enskild_firma" | "aktiebolag",
|
|
org_number: string,
|
|
vat_registered: boolean,
|
|
moms_period: "monthly" | "quarterly" | "yearly",
|
|
accounting_method: "accrual" | "cash",
|
|
fiscal_period: { start_date: string, end_date: string, name: string },
|
|
team_id: 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[]
|
|
}
|
|
}
|
|
```
|