---
title: "Create Storage Connection"
method: POST
path: "/companies/{company_id}/storage-connections"
tags: ["companies"]
---

# Create Storage Connection

`POST /companies/{company_id}/storage-connections`

## 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/companies/{company_id}/storage-connections`
- Auth: none declared

Add a storage connection from typed configuration (platform admin). First one becomes
default; secret encrypted, never returned.

A company's owners and accountants connect their OAuth drives (Dropbox, Google Drive,
OneDrive, Box) with ``POST /storage-connections/oauth/{provider}/authorize`` instead —
this create also accepts service-account / app-only modes and server-side providers
(local, SharePoint, WebDAV, S3), which stay with a platform administrator (ZET-434).

## Path parameters

- `company_id` string, required

## Headers

- `authorization` string, nullable

## Cookies

- `access_token` string, nullable

## Request body

- StorageConnectionCreate: ``POST /companies/<id>/storage-connections`` body (admin). The **first** storage connection of a company becomes its default automatically (service layer). ``credential`` is write-only; whether it is required or forbidden is a **config-aware, per-provider** decision enforced by the strategy's ``validate_create`` hook in the service (foundation punch-list P0 #3) — not a static schema rule, because gdrive/onedrive/box key it off ``config.auth_mode`` and webdav off ``config.flavor``.
  - `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

## Response `201`

Successful Response

- StorageConnectionResponse: ``GET/POST/PUT /storage-connections/...`` → ``{"storage_connection": ...}``.
  - `storage_connection` StorageConnectionOut, required: Serialized storage connection (secret-free: NO credential, NO token).
    - `id` string, required
    - `company_id` string, required
    - `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.
    - `supports_ingest` boolean
    - `label` string, required
    - `enabled` boolean, required
    - `is_default` boolean, required
    - `config` object, required
    - `status` 'connected' | 'reconnect_required' | 'error', required: Operational state of a storage connection (cloud-storage foundation §1). ``connected`` = healthy; ``reconnect_required`` = a revoked/expired credential needs operator re-auth (surfaces a Connect/Reconnect affordance, never a silent retry); ``error`` = a transient/other failure recorded in ``last_error``.
    - `resource_root_id` string, nullable
    - `token_expiry` string, date-time, nullable
    - `last_success_at` string, date-time, nullable
    - `last_error` string, nullable
    - `provider_account_id` string, nullable
    - `path_template_payable` string, nullable
    - `file_template_payable` string, nullable
    - `path_template_receivable` string, nullable
    - `file_template_receivable` string, nullable

## 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)
