---
title: "Create Company"
method: POST
path: "/onboarding/companies"
tags: ["onboarding"]
---

# Create Company

`POST /onboarding/companies`

## Request

- Base URL: `https://app.zeno-works.ch` (the document declares no server; this is the origin it was published from)
- URL: `https://app.zeno-works.ch/onboarding/companies`
- Auth: none declared

Create a company with its books — **this is the one to use**.

**There are two `create_company` operations.** This one collects everything a
company needs to be booked in: chart of accounts, fiscal year, opening balances,
VAT setup, optional integrations and payroll — and grants the creator `owner`.
Callable by an administrator, an **owner of any company**, or a user who is a
member of none.

The other, `companies_create_company` (`POST /companies/`), is **admin-only** and
writes the tenant row and nothing else — no chart, no fiscal year, no opening
balances, no membership. A company created that way cannot be booked in and
nobody can see it. Reach for it only when an administrator will populate the
company by other means.

The sole write path of onboarding. Persists company + integrations + accounting
(settings, VAT, chart, balances, opening entries), posts the opening entries to
the GL, grants the creator self-membership, and consumes the caller's resumable
draft — all in one ``db.tx()``, so any failure rolls everything back (keeping the
draft). Runs under the SystemPrincipal elevation (module docstring); ``user.id``
is passed explicitly for provenance/membership/draft consumption. Rate-limited
per user (10/min — one legitimate create per run); non-admin creators re-check
creation-quota inside the transaction (advisory lock — module docstring TOCTOU
note). **409** if the CREATOR already has a company by that name
(``ConflictError`` → global handler; scoped per creator, so it discloses nothing
about other tenants); **422** if the
collected data cannot be persisted (e.g. unbalanced opening entries, or opening
entries the opening-entries preview refuses), the caller gained a membership since
the gate (concurrent create), or a non-admin sends ``integrations``.

**An API-key caller may not send ``opening_entries``** (422
``forbidden.openingEntriesFromApiKey``). Keep the prior year's closing sheet in
``balance_snapshot`` and the first year to book in ``target_fiscal_year``, leave
``opening_entries`` out, then call ``generate_opening_entries`` and
``post_opening_entries`` — Zeno builds the opening from the balance sheet.

## Query parameters

- `on_behalf_of_user_id` string, nullable

## Headers

- `authorization` string, nullable

## Cookies

- `access_token` string, nullable

## Request body

- OnboardingCompanyCreate: ``POST /onboarding/companies`` — the entire collected company data structure. Accounting sections are individually optional, so a minimal company (identity + integrations + settings) is creatable and partial setups persist cleanly. The whole create runs in one transaction: any failure rolls everything back.
  - `company` OnboardingCompanyIn, required: Wizard company identity — ``industry`` is **required** here (ZET-157). The admin ``POST /companies/`` body (:class:`CompanyCreate`) keeps ``industry`` nullable, and ``CompanyUpdate``'s explicit-null-clears semantics are untouched — the requirement is wizard-only. Rationale: a null industry seeded the 72-account base chart with no industry overlay (e.g. no 3400 Dienstleistungserlöse for a services tenant) and left ``ar_revenue_default_account_code`` at the base 3000, and the gap only surfaced at the first revenue booking. Any non-blank string is accepted — unknown/freeform keys (including the wizard's "Other / not listed" fallback) still resolve to the base chart, but that is now an explicit choice, never a silent default.
    - `company_name` string, required
    - `active` boolean
    - `ingest_lookback_months` 3 | 6 | 12, nullable
    - `language` 'de' | 'en' | 'fr' | 'it', nullable
    - `industry` string, required
  - `integrations` OnboardingIntegrationsIn: Optional integration accounts/drives to create with the company.
    - `email` EmailAccountCreate[]
      - `label` string, required
      - `imap_user` string, required
      - `imap_password` string, required
      - `imap_host` string, required
      - `imap_port` integer
      - `imap_tls` boolean
      - `imap_inbox` string
      - `imap_archive` string, nullable
      - `enabled` boolean
      - `poll_batch_cap` integer, nullable
      - `allow_private_host` boolean, nullable
    - `epost` EpostAccountCreate[]
      - `label` string, required
      - `epost_api_key` string, required
      - `enabled` boolean
      - `poll_batch_cap` integer, nullable
    - `bexio` BexioAccountCreate[]
      - `label` string, required
      - `bexio_api_token` string, required
      - `base_url` string
      - `enabled` boolean
      - `poll_batch_cap` integer, nullable
    - `abacus` AbacusAccountCreate[]
      - `label` string, required
      - `base_url` string, required
      - `mandant` string, required
      - `interface_version` string
      - `service_user` string, required
      - `service_password` string, required
      - `fetch_journal` boolean
      - `fetch_invoices` boolean
      - `fetch_documents` boolean
      - `poll_batch_cap` integer, nullable
      - `feeds_config` object, nullable
      - `enabled` boolean
      - `allow_private_host` boolean, nullable
    - `storage_connections` StorageConnectionCreate[]
      - `provider` 'sharepoint' | 'local' | 'dropbox' | 'gdrive' | 'onedrive' | 'box' | 'webdav' | 's3' | 'platform', required: Which storage backend a :class:`~invoiage_db.models.company.StorageConnection` dispatches to (the unified archive-destination model — cloud-storage foundation). ``sharepoint`` (Graph app-only) and ``local`` (mounted volume) are the two behavior-preserving providers folded in from the old per-table drives; the remaining values are reserved for the later provider driver plans. ``platform`` is the **system-managed** default archive target (doc/architecture.md §"Platform archive"): a platform-owned S3 bucket with a per-company key prefix, seeded for every company at creation so archival always has a destination. Its endpoint/bucket/credentials live in platform settings (``PLATFORM_ARCHIVE_*``), never per-row; tenants cannot create, mutate, or delete these rows.
      - `label` string, required
      - `config` union, required
        - SharePointConfig
          - `provider` 'sharepoint'
          - `tenant_id` string, required
          - `client_id` string, required
          - `site_url` string, required
          - `document_library` string, required
        - LocalConfig
          - `provider` 'local'
          - `root_path` string, required
        - OneDriveConfig: OneDrive (Microsoft Graph) non-secret config — delegated OAuth + app-only. No secrets here (the delegated refresh token / app-only client secret live in ``credential_enc``). ``resource_root_id`` on the connection holds the resolved Graph driveId after connect; ``build_client`` falls back to resolving from these fields.
          - `provider` 'onedrive'
          - `auth_mode` 'delegated' | 'app_only'
          - `tenant` string
          - `scope` 'files_readwrite' | 'files_readwrite_all'
          - `tenant_id` string, nullable
          - `client_id` string, nullable
          - `drive_kind` 'me' | 'user' | 'site' | 'drive'
          - `user_upn` string, nullable
          - `site_url` string, nullable
          - `document_library` string, nullable
          - `drive_id` string, nullable
        - DropboxConfig: Dropbox non-secret config — OAuth2 auth-code + refresh token (§05-dropbox). No secrets here (the OAuth refresh token lives in ``credential_enc``). ``full_dropbox`` is SERVER-AUTHORITATIVE (not tenant-controlled): the authorize step forces it from the platform ``DROPBOX_ACCESS_TYPE`` env, so the driver knows whether to send a ``Dropbox-API-Path-Root`` header without a re-lookup. ``resource_root_id`` on the connection holds the Full-Dropbox root namespace id (NULL for an App-Folder app).
          - `provider` 'dropbox'
          - `path_root` string, nullable
          - `full_dropbox` boolean
        - GDriveConfig: Google Drive non-secret config — OAuth2 offline refresh OR Workspace service account. No secrets here: the OAuth refresh token (``auth_mode=oauth``) or the service-account JSON key (``auth_mode=service_account``) live encrypted in ``credential_enc``. The archive-root Drive folder id lands in ``resource_root_id`` post-consent (bootstrapped for ``drive.file``, or the chosen folder / Shared-Drive id otherwise). ``scope`` defaults to least-privilege ``drive.file`` (the app sees only files it created — ideal for an archiver building its own tree). Restricted ``drive`` (arbitrary pre-existing folder / Shared Drive) triggers Google verification, so it is opt-in.
          - `provider` 'gdrive'
          - `auth_mode` 'oauth' | 'service_account'
          - `scope` 'drive.file' | 'drive'
          - `root_folder_name` string
          - `shared_drive_id` string, nullable
          - `delegated_user` string, nullable
        - BoxConfig: Box non-secret config — user rotating-refresh OAuth OR enterprise CCG app-only (§07-box). No secrets here: the rotating single-use refresh token (``auth_mode=user``) or the CCG server-auth ``client_secret`` (``auth_mode=enterprise``) live encrypted in ``credential_enc``. Box is id-based — ``root_folder_id`` (``"0"`` = All Files root) is the target Box folder id; ``resource_root_id`` on the connection holds the chosen folder id (may be NULL for a freshly-CRUD-created enterprise connection → readers fall back here). The enterprise fields (``client_id`` + ``enterprise_id``, optional admin-approved ``as_user_id`` impersonation) name the customer's own Platform (server-auth) app; user mode uses the platform app (``BOX_CLIENT_ID``/``SECRET``) and must not carry them.
          - `provider` 'box'
          - `auth_mode` 'user' | 'enterprise'
          - `root_folder_id` string
          - `client_id` string, nullable
          - `enterprise_id` string, nullable
          - `as_user_id` string, nullable
        - WebDavConfig: Nextcloud + generic WebDAV non-secret config (§08-webdav §1). One provider (``webdav``) covers both Nextcloud and any generic WebDAV collection: both authenticate with HTTPS Basic auth (username + app-password/token) — the app-password / token is the single renewable secret and lives in ``credential_enc``, NEVER here. A ``flavor`` field carries the only two differences: how the credential is obtained (Nextcloud Login Flow v2 vs. user-pasted) and whether a server-side revoke exists. ``base_url`` is the WebDAV files collection (Nextcloud: ``https://cloud.example.com/remote.php/dav/files/alice/``; generic: the raw endpoint). ``server_url`` is the Nextcloud base (``https://cloud.example.com``) used for the OCS app-password revoke + Login-Flow endpoint derivation; required for ``flavor=nextcloud``. Both URLs are https-only (guide §6.2 — Basic credentials must never cross plain HTTP). The URL fields validate as :class:`HttpUrl` but serialize to plain strings so ``config`` stays JSON-clean in JSONB / ``*Out``.
          - `provider` 'webdav'
          - `flavor` 'nextcloud' | 'generic'
          - `server_url` string, uri, nullable
          - `base_url` string, uri, required
          - `username` string, required
          - `root_path` string, nullable
          - `verify_tls` boolean
        - S3Config: S3-compatible object-storage non-secret config (§09-s3 §2). An **application storage backend, not a user-drive OAuth provider** — the same static/role family as ``webdav``/``local`` (``uses_access_token=False``, ``supports_oauth=False``). No secret here: the **static** secret access key (static-key mode) lives encrypted in ``credential_enc``; workload and cross-account STS assume-role modes keep ``credential_enc`` NULL (boto3/botocore auto-renew credentials internally). The credential **mode is DERIVED from config**, never a stored flag (§0): - neither ``role_arn`` nor ``access_key_id`` ⇒ ``workload`` (boto3 default chain: ECS task role / EC2 instance / EKS IRSA); - ``role_arn`` set ⇒ ``assume_role`` (cross-account STS AssumeRole with ``external_id``); - ``access_key_id`` set ⇒ ``static_key`` (the SECRET access key rides ``credential_enc``). ``external_id`` is a **non-bearer confused-deputy guard** (guide §7), useless without also being the platform principal named in the customer's role trust policy, so it lives in ``config`` (echoed by ``*Out`` for the customer to paste into their trust policy) and is redacted from every log line / ``last_error``. ``endpoint`` (when set) is a non-AWS S3-compatible endpoint (MinIO/Wasabi/Backblaze/Ceph) passed verbatim to botocore; it must be ``https://`` unless the deployment sets ``S3_ALLOW_PLAINTEXT_ENDPOINT`` (e.g. a dev stack's in-cluster MinIO at ``http://blobstore:9000``) — else create/update is 422.
          - `provider` 's3'
          - `endpoint` string, nullable
          - `region` string, required
          - `bucket` string, required
          - `prefix` string, nullable
          - `path_style` boolean
          - `role_arn` string, nullable
          - `external_id` string, nullable
          - `access_key_id` string, nullable
          - `sse` 'AES256' | 'aws:kms', nullable
          - `sse_kms_key_id` string, nullable
        - PlatformConfig: System-managed platform-archive non-secret config (doc/architecture.md §"Platform archive"). The ONLY per-row field is the per-company key ``prefix`` (``companies/<id>``); bucket/endpoint/region/credentials are resolved from platform settings (``PLATFORM_ARCHIVE_*``) at client-build time — never stored per row, so the archive location can be rotated centrally without touching any company row. ``credential_enc`` is always NULL (credentials are ambient platform config). Rows of this provider are seeded by the system and are immutable through the tenant CRUD surface (strategy ``validate_create`` + service guards).
          - `provider` 'platform'
          - `prefix` string, required
      - `credential` string, nullable
      - `is_default` boolean
      - `enabled` boolean
      - `path_template_payable` string, nullable
      - `file_template_payable` string, nullable
      - `path_template_receivable` string, nullable
      - `file_template_receivable` string, nullable
  - `settings` OnboardingSettingsIn: The accounting settings the wizard collects (defaults applied in the service).
    - `fiscal_year_start_month` integer
    - `base_currency` string
    - `opening_balance_account_number` string
    - `ar_revenue_default_account_code` string, nullable
    - `vat_liable` boolean, nullable
    - `settlement_method` 'effective' | 'net_tax_rate' | 'flat_tax_rate' | 'unknown'
    - `timing_basis` 'agreed' | 'collected' | 'unknown'
    - `filing_period_type` 'monthly' | 'quarterly' | 'semi_annual' | 'annual' | 'unknown'
    - `uid` string, nullable
    - `canton` string, nullable
    - `vat_period_start` string, date, nullable
    - `vat_period_end` string, date, nullable
    - `ar_default_due_days` integer, nullable
  - `vat_rates` OnboardingVatRateIn[]
    - `code` string, nullable
    - `rate_bps` integer, required
    - `name` string, nullable
    - `kind` string, nullable
    - `revenue_account_code` string, nullable
    - `valid_from` string, date, nullable
    - `valid_to` string, date, nullable
    - `active` boolean
  - `gl_accounts` OnboardingChartAccountIn[]
    - `account_number` string, required
    - `account_name` string, nullable
    - `category` 'asset' | 'liability' | 'equity' | 'income' | 'expense' | 'tax' | 'other' | 'unknown': The account-type axis of a ledger account (Swiss-KMU chart of accounts). Distinct from — but derived from — ``LineRole`` (the journal-line semantic role). ``unknown`` is the conservative default when classification can't decide; ``other`` covers genuinely ambiguous / closing (9xxx) accounts.
    - `is_profit_loss` boolean
    - `is_contra` boolean
    - `is_intentional` boolean
    - `account_number_guess` string, nullable
    - `journal_usage_count` integer
    - `prior_year_turnover_cents` integer, nullable
    - `prior_year_turnover_fiscal_year` integer, nullable
  - `bank_accounts` OnboardingBankAccountIn[]
    - `iban` string, required
    - `label` string, nullable
    - `currency` string, nullable
    - `holder_name` string, nullable
    - `bic` string, nullable
    - `bank_name` string, nullable
    - `ledger_account_number` string, nullable: Chart-of-accounts code (e.g. "1020") the bank side of a journal entry posts to; surrounding whitespace is trimmed. It is checked against the chart this create writes (your gl_accounts, or the bundled industry template when you send none). A code that is not in that chart, or is in it but is not a balance-sheet (asset/liability, non-contra) account, is NOT persisted: the bank account is still created, unmapped, and the create reports it in summary.bank_mappings_unresolved as "<iban>: <code> (<reason>)" with reason not_in_chart or not_a_bank_ledger_account. The create does not fail. An unmapped account can be mapped later on the Banking page.
  - `bank_completeness` OnboardingBankCompletenessIn: F2478 (1) — the bank step's confirmation (owner decision 2026-09-27): the bank accounts declared in this onboarding are ALL of the company's business accounts, and ``takings`` says whether cash / payment-provider takings exist and are imported too (``imported``) or do not exist (``none``). Stored once, dated, with the onboarding user (``vat.bank_completeness``); the SSS entry test proves a quiet stretch nil from these accounts' statements only. Omitted = not confirmed (the Settings page can do it later).
    - `takings` 'none' | 'imported', required
  - `documents` OnboardingDocumentIn[]
    - `staging_ref` string, required
    - `filename` string, required
    - `content_type` string, nullable
    - `size` integer, nullable
    - `fiscal_year_hint` integer, nullable
    - `doc_types_found` string[]
    - `result_summary` object, nullable
  - `balance_snapshot` OnboardingBalanceSnapshotIn: A balance-sheet snapshot (analyze output and create input). On analyze output ``resolution_json`` / ``equity_roles`` / ``source_document_ref`` are unset; the wizard fills them after the resolve-balance step and resends them.
    - `fiscal_year` integer, required
    - `as_of_date` string, date, nullable
    - `currency` string, required
    - `lines` OnboardingBalanceLineIn[]
      - `line_id` string, nullable
      - `account_number` string, required
      - `account_name` string, nullable
      - `category` 'asset' | 'liability' | 'equity' | 'income' | 'expense' | 'tax' | 'other' | 'unknown': The account-type axis of a ledger account (Swiss-KMU chart of accounts). Distinct from — but derived from — ``LineRole`` (the journal-line semantic role). ``unknown`` is the conservative default when classification can't decide; ``other`` covers genuinely ambiguous / closing (9xxx) accounts.
      - `side` 'credit' | 'debit', required
      - `amount_cents` integer, required
      - `sign_display` string, nullable
      - `needs_review` boolean
      - `is_contra` boolean
      - `comparative_amount_cents` integer, nullable
    - `totals` object
    - `resolution_json` object, nullable
    - `equity_roles` object, nullable
    - `source_document_ref` string, nullable
    - `comparative_period_label` string, nullable
    - `founding` boolean, nullable: ZET-395: the answer to 'founding balance sheet (Gründungsbilanz) or closing sheet?'. true = the company's founding balance sheet: stored as its own kind (status 'founding') under its fiscal_year, which must be the create's first year, and that year's opening is generated from it on its as_of_date. false = the year's closing sheet (the historical behaviour). Asked for when as_of_date lies inside the company's first fiscal year before its end: left unset, the sheet is held for the question under Settings -> Opening (a pending change set with summary.founding_question) instead of becoming the year's closing sheet, and opening entries are refused (422). Not needed for a sheet dated on the year-end. Unset on analyze output.
  - `fiscal_year_summary` OnboardingFiscalYearSummaryIn: A P&L summary (analyze output and create input).
    - `fiscal_year` integer, required
    - `net_result_cents` integer, nullable
    - `period_start` string, date, nullable
    - `period_end` string, date, nullable
  - `opening_entries` OnboardingOpeningEntriesIn: The final (reviewed/edited) opening-entry proposal to persist + post to the GL.
    - `target_fiscal_year` integer, required
    - `opening_posting_date` string, date, required
    - `status` string
    - `request_json` object
    - `request_hash` string
    - `response` object, required
    - `accountant_metadata` object, nullable
  - `open_items` OnboardingOpenItemIn[]
    - `direction` 'receivable' | 'payable', required
    - `counterparty_name` string, required
    - `amount_cents` integer, required
    - `invoice_number` string, nullable
    - `issue_date` string, date, nullable
    - `due_date` string, date, nullable
    - `vat_rate_bps` integer, nullable
    - `vat_amount_cents` integer, nullable
    - `vat_basis` 'agreed' | 'collected', nullable
    - `vat_method` 'effective' | 'net_tax_rate' | 'flat_tax_rate', nullable
    - `supply_date` string, date, nullable
    - `invoice_gross_cents` integer, nullable
    - `vat_recognised_cents` integer, nullable
    - `staging_ref` string, nullable
  - `issuer` OnboardingIssuerIn, required: The invoice-sender identity — **the address is required** (ZCT-513). Seeds the company's default issuer profile (``invoiage_core.ar.defaults``) in the atomic create, and the remaining fields (VAT number, creditor bank account, the creator's email) are enriched on top of it, fill-only. ⚖️ **Owner ruling 2026-09-03.** ``address_line1``/``postal_code``/``city`` used to be optional, the seeded profile was written with an empty address on purpose, and the gap surfaced only when someone tried to issue — which is how FenX AG reached a finished draft it could not send (ZCT-513). They are now required and non-blank. The three are exactly what issuing needs end to end: ``ar.validation`` blocks on a missing name/address, and ``qrbill`` refuses a combined address whose second line (``"<postal_code> <city>"``) is empty, silently dropping the Swiss payment part (ZCT-541). ``address_line2``, ``country`` and the contacts stay optional. 🔴 **A deliberate, owner-reaffirmed breaking change**, recorded here so it is not "fixed" back: a programmatic create that omits the address now 422s. That includes ``onboarding_create_company`` over MCP — the path FenX AG and Felina Food AG were created through, and the one the ZCT test cycle uses. Blankness is checked as well as presence, for the reason ``OnboardingCompanyIn. industry`` gives below: ``min_length`` alone accepts ``" "``, and a whitespace address is the same broken invoice as no address, arrived at through the same empty form field.
    - `address_line1` string, required
    - `address_line2` string, nullable
    - `postal_code` string, required
    - `city` string, required
    - `country` string, nullable
    - `contact_email` string, nullable
    - `contact_phone` string, nullable
  - `target_fiscal_year` integer, nullable
  - `payroll` OnboardingPayrollIn: Company payroll setup: every sub-section optional (partial setup is fine — run validation names what is missing). The statutory rate schedule and the wage-type catalog are auto-seeded at company create; this adds only the company-specific insurer planes on top.
    - `settings` OnboardingPayrollSettingsIn: The payroll posting-account keys + delivery mode + the QST 13th-salary method + the salary proration basis (all optional — absent keys keep the SP2 catalog defaults, and an absent method keeps ``ks45_frequency``, KS 45 §6.3; a Swissdec-certified company picks ``swissdec_periodic`` here instead of after creation).
      - `salary_payable_account_code` string, nullable
      - `payroll_clearing_account_code` string, nullable
      - `qst_liability_account_code` string, nullable
      - `employee_receivable_account_code` string, nullable
      - `payslip_delivery_mode` 'download' | 'email': How a company delivers payslips (workflow gap #6 — H9/H11). ``download`` (default) = the accountant downloads/hands out the PDFs; ``email`` = the post handler's best-effort phase emails each rendered slip to ``gl_employee.email`` (skips without an address are surfaced, never fatal — the retry endpoint covers stragglers).
      - `qst_thirteenth_method` 'ks45_frequency' | 'swissdec_periodic': How the QST rate-determining income treats a 13th salary (F2644; plan D4). ``ks45_frequency`` (default) = KS 45 §6.3 / §6.6: an annual 13th is converted to the 360-day period it is paid for (``13th / insured days YTD × 360``) and a monthly accrual is part of the month's gross; ``swissdec_periodic`` = ELM 6.0 §10.6.1.2: the 13th is a periodic item, extrapolated with the month's other periodic pay (declared Swissdec-certified software; the Swissdec worked examples M17–M22). The two differ by up to 2,500 of rate-determining income on the M17 facts.
      - `salary_proration` 'calendar' | '30' | 'working': The day basis a derived monthly salary is prorated on (F2752). ``calendar`` (default, what the product always did) = actual calendar days (act/act: 15 of 31 days is 15/31); ``30`` = the 30/360 convention of the ELM insured days and KS 45 §6.6 (a whole month is 30 days, the 31st and 28/29 February count as the 30th: 15 of 31 days is 15/30); ``working`` = Monday-to-Friday working days of the employed window over those of the month (no holiday calendar). One company-wide choice: it prorates hire/exit months and the salary days an unpaid-leave or EO event removes.
    - `uvg` UvgPolicyCreate
      - `insurer_name` string, required
      - `policy_number` string, nullable
      - `valid_from` string, date, required
      - `valid_to` string, date, nullable
      - `rates` UvgRateIn[], required
        - `solution_code` string, required
        - `description` string, nullable
        - `bu_employer_bps` number, required
        - `nbu_bps` number, required
        - `nbu_paid_by` 'employee' | 'employer'
    - `ktg` KtgPlanCreate
      - `code` string, required
      - `name` string, required
      - `insurer_name` string, nullable
      - `policy_number` string, nullable
      - `version` KtgPlanVersionCreate, required
        - `employee_bps` number, required
        - `employer_bps` number, required
        - `annual_salary_cap` union
          - number
          - string
        - `valid_from` string, date, required
        - `valid_to` string, date, nullable
    - `fak` FakAffiliationCreate
      - `canton` string, required
      - `fund_name` string, nullable
      - `employer_bps` number, required
      - `child_allowance_month` union, required
        - number
        - string
      - `education_allowance_month` union, required
        - number
        - string
      - `valid_from` string, date, required
      - `valid_to` string, date, nullable
    - `ausgleichskasse` AusgleichskasseCreate: The company's Ausgleichskasse and its VK scale over a validity window. ``bands`` ascend by ``from``; the whole-sum rule applies (the band the annual selector falls in is ONE rate on the whole base). ``band_basis`` says what selects the band; ``forecast_basis`` (CHF per year) is the employer's expected selector the run accrual uses — required for a scale with more than one band. Authoritative validation: ``rates/service/ausgleichskasse.py`` (the legal ceiling is the catalog key ``ahv_vk_max_rate``).
      - `name` string, required
      - `kasse_number` string, nullable
      - `band_basis` 'contribution_sum' | 'payroll'
      - `bands` VkBandInInput[], required
        - `from_chf` union, required
          - number
          - string
        - `rate_pct` union, required
          - number
          - string
      - `forecast_basis` union
        - number
        - string
      - `valid_from` string, date, required
      - `valid_to` string, date, nullable
    - `bvg` BvgPlanCreate
      - `code` string, required
      - `name` string, required
      - `provider_name` string, nullable
      - `version` BvgPlanVersionCreate, required
        - `entry_threshold_override` union
          - number
          - string
        - `entry_threshold_inclusive` boolean
        - `partial_month_billing` 'full_month' | 'calendar_days'
        - `coordination_deduction_override` union
          - number
          - string
        - `upper_limit_override` union
          - number
          - string
        - `age_tiers_override` BvgAgeTierIn[], nullable
          - `age_min` integer, required
          - `age_max` integer, required
          - `total_bp` number, required
        - `extra_total_bps` number
        - `employee_share_bps` number
        - `valid_from` string, date, required
        - `valid_to` string, date, nullable
    - `auto_run_enabled` boolean, nullable
  - `employees` OnboardingEmployeeIn[]
    - `ahv_number` string, nullable
    - `birthdate` string, date, nullable
    - `email` string, nullable
    - `sex` 'male' | 'female': AHV/Swissdec registers are M/F only.
    - `street` string, nullable
    - `postal_code` string, nullable
    - `town` string, nullable
    - `country` string, nullable
    - `nationality` string, nullable
    - `work_permit` 'none' | 'b' | 'c' | 'g' | 'l' | 'f' | 'n' | 'ci' | 'other': Work-permit category; ``none`` = Swiss citizen (no permit needed).
    - `marital_status` 'single' | 'married' | 'divorced' | 'widowed' | 'separated' | 'registered_partnership' | 'dissolved_partnership'
    - `marital_status_valid_from` string, date, nullable
    - `spouse_has_income` boolean, nullable
    - `church_tax` boolean, nullable
    - `residence_canton` string, nullable
    - `residence_municipality_bfs` integer, nullable
    - `residence_municipality_name` string, nullable
    - `workplace_canton` string, nullable
    - `qst_canton` string, nullable
    - `qst_tariff_code` string, nullable
    - `employment_start` string, date, nullable
    - `employment_end` string, date, nullable
    - `department_dimension_value_id` string, nullable
    - `employee_no` string, nullable
    - `display_name` string, required
    - `qst_liable` boolean
    - `other_employment` boolean
    - `contract` PayrollContractCreate
      - `valid_to` string, date, nullable
      - `workload_percent` union
        - number
        - string
      - `monthly_salary` union
        - number
        - string
      - `hourly_rate` union
        - number
        - string
      - `hourly_holiday_pay_percent` union
        - number
        - string
      - `thirteenth_salary` boolean
      - `thirteenth_schedule` 'december' | 'monthly_accrual' | 'half_yearly' | 'quarterly': 13th-salary payout schedule (mirrors SP4's ``ThirteenthSchedule``); default ``december`` = December payout + exit pro-rata (MASTER §7 P2). ``half_yearly`` pays at the end of June and December (+ exit), ``quarterly`` at the end of March, June, September and December (+ exit) — F2681, ``engine/thirteenth.py``.
      - `bvg_plan_id` string, nullable
      - `bvg_declared_annual_salary` union
        - number
        - string
      - `uvg_solution_code` string, nullable
      - `ktg_plan_id` string, nullable
      - `nbu_exempt` boolean
      - `insurance_policy` object
      - `cost_center_dimension_value_id` string, nullable
      - `valid_from` string, date, required
      - `salary_model` 'monthly' | 'hourly', required
      - `close_predecessor` boolean
    - `iban` string, nullable
    - `ktg_plan_code` string, nullable
    - `bvg_plan_code` string, nullable

## Response `201`

Successful Response

- OnboardingCreateResponse: ``POST /onboarding/companies`` → the new company id + a summary of what was created.
  - `company_id` string, required
  - `summary` OnboardingCreateSummary, required: Per-section counts of what the atomic create persisted.
    - `integrations` integer
    - `vat_rates_seeded` integer
    - `vat_rate_overrides` integer
    - `gl_accounts` integer
    - `bank_accounts` integer
    - `bank_completeness_confirmed` boolean
    - `documents` integer
    - `balance_lines` integer
    - `fiscal_year_summary` boolean
    - `opening_entries_posted` integer
    - `gl_entries_posted` integer
    - `gl_entry_id` string, nullable
    - `chart_template` string, nullable
    - `chart_template_codes_seeded` string[]
    - `bank_mappings_unresolved` string[]: Declared bank_accounts[].ledger_account_number values this create did NOT persist, one entry per dropped mapping: "<iban>: <code> (<reason>)", where reason is not_in_chart (the code is not in the company's chart) or not_a_bank_ledger_account (it is, but not as a balance-sheet asset/liability, non-contra account). The bank account itself was created, unmapped. Empty on a clean create.
    - `open_items_created` integer
    - `ar_defaults_seeded` boolean
    - `payroll_planes_configured` integer
    - `employees_created` integer
    - `contracts_created` integer

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/zeno-works/apis/zeno-api.md) · [All operations](https://skmtc.dev/zeno-works/apis/zeno-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zeno-works/apis/zeno-api/revisions/1eb93dde146c?raw)
