{"openapi":"3.1.0","info":{"title":"Craftivo Public API","version":"1.0.0","summary":"Stable v1 REST API for tasks, members, customers, customer requests, projects, forms, documents, and webhooks.","description":"Source of truth for the Craftivo public API surface (CI-1636). Every route\nthat exists in `convex/http_api_v1.ts` is described here — a contract test\n(`convex/http_api_v1.contract.test.ts`) compares both directions and fails\nCI when this file and the code drift apart. Customer requests are a\nfirst-class resource alongside customers and projects.\n\n## Authentication\n\nSend the API key as a bearer token: `Authorization: Bearer <api key>`.\nKeys are created by the organization owner in Settings → \"API & Webhooks\".\nAPI access itself requires the Business plan or, on Starter and Premium, the\nAPI add-on; requests from other organizations are rejected with HTTP 403\n(`plan_feature_required`). `GET /api/v1/me` stays open on every plan so\nintegrations can discover the account. With the API add-on the quota is\n5,000 requests per day and organization and 60 per minute and key.\n\nOnce the trial of a Business organization ends without a subscription, or a\ncancelled API add-on reaches the end of its paid period, writes are rejected\nwith HTTP 402 (`billing_access_required`) and webhook events of an ended\nadd-on are no longer delivered. Reads and deleting a webhook\n(`DELETE /api/v1/webhooks/{webhookId}`) keep working, so an integration can\nstill unsubscribe.\n\n## API key scopes\n\n- `read` — only `GET` requests are allowed. `POST`, `PUT`, `PATCH` and\n  `DELETE` are rejected with HTTP 403 (`insufficient_scope`) before the\n  request touches any data (a replayed `Idempotency-Key` is rejected too).\n- `read_write` — full access to the endpoints below, except those that need\n  an extra right.\n\nFour actions are irreversible or reach real customers, so `read_write`\nalone does not allow them. The organization owner enables each one per key\nunder Settings > API & Webhooks:\n\n- `documents:finalize` — assign the document number and lock the document.\n- `documents:send` — send documents to customers by email.\n- `payments:write` — record payments on invoices.\n- `payments:correct` — book corrections that reverse payments. `payments:write`\n  alone does not allow it.\n\nAn endpoint that needs a right is rejected with HTTP 403\n(`insufficient_scope`) without it, before the request touches any data (a\nreplayed `Idempotency-Key` is rejected too). `details.requiredScope` names\nthe missing right and `details.extraRights` lists the rights the key has.\nA `read` key always gets `requiredScope: read_write` and can hold no extra\nrights, and neither can an unbound legacy key. `GET /api/v1/me` returns\nthe scope and rights of the key.\n\nException: the `/api/v1/webhooks` endpoints accept **both** scopes. An\nintegration with a read-only key must be able to subscribe and unsubscribe\nitself; a webhook never exposes data the key could not read anyway.\nA webhook delivers data with the rights of the member who created it. A key\ntherefore sees and manages only the webhooks its own member created, unless\nthat member is currently an owner (`inhaber`); any other webhook answers 404.\n\nKeys created in the settings UI are bound to exactly one organization; a\nrequest carrying a different `X-Organization-Id` is rejected with HTTP 403\n(`forbidden`). Keys issued before scopes existed are unbound and behave as\n`read_write`.\n\nThe owner can pause a key in the settings without deleting it. Every\nrequest with a paused key — including `GET /api/v1/me` — is rejected with\nHTTP 403 (`api_key_paused`) before any rate limit is checked, so a paused\nkey consumes no quota. Reactivating the key restores access immediately.\n\n## Permissions of the key's creator\n\nBesides the key scope, every request is limited by the role and the\npermissions the key's creator has in the organization, the same ones that\napply in the craftivo app. `GET /api/v1/me` lists them per organization in\n`permissions`: a yes/no permission as its key (`financials`), a graded\npermission as `key:level` (`projects:own`). Permissions that are not\ngranted, and the level `none`, are left out.\n\nA request the creator's permissions do not allow is rejected with HTTP 403\n(`forbidden`). When the missing permission is known,\n`details.requiredPermission` names it in the same notation. A graded key\nwithout a level (`projects`) means any level other than `none`.\n\n## Envelope\n\nSuccessful responses are wrapped in `{ \"data\": … }`, list responses add\n`{ \"meta\": { \"cursor\", \"isDone\", \"limit\" } }`. Every error — including\nunknown routes and unexpected internal failures — is returned as\n`{ \"error\": { \"code\", \"message\", \"details\"? } }` with an English message.\n\n## Conventions\n\n- Timestamps are ISO 8601 in UTC (`createdAt`, `updatedAt`); date-only\n  fields use `YYYY-MM-DD`.\n- Monetary amounts are integer cents plus a `currency` field (today `EUR`).\n- Lists are sorted newest first. A filter without matches returns HTTP 200\n  with an empty `data` array, never a 404.\n- `limit` is clamped server-side to 1…100 and defaults to 50.\n- Unknown paths return HTTP 404 (`not_found`); a known path called with an\n  unsupported method returns HTTP 405 (`method_not_allowed`).\n\n## Idempotency\n\n`POST` and `PUT` accept an `Idempotency-Key` header. A repeated request\nwith the same key replays the stored response and carries the response\nheader `Idempotency-Replayed: true`.\n\n- A key applies per API key, method and path. The same key on another path\n  is a new request; it never replaces the stored response of the first\n  path. The first stored response wins.\n- Another API key of the organization that sends the same `Idempotency-Key`\n  never gets the stored response. Its request runs as a new request, under\n  its own permissions. A retry must use the same API key as the first\n  request; a new API key starts over.\n- `POST /schedule-entries`, `POST /maintenance-contracts/{contractId}/schedule-visits`,\n  `POST /maintenance-visits/{visitId}/complete` and `POST /absences` check the\n  permissions of the caller first and answer from the current record, under\n  the caller's current read permissions. A key without the permission gets\n  `403`, also for an `Idempotency-Key` it used before. A replay carries\n  no `meta.conflicts`; a record deleted in the meantime answers `404`.\n  `complete` answers a repeat with `alreadyCompleted: true`.\n- All other `POST` and `PUT` routes check the current permissions of the\n  caller before they replay the stored response. A key without the permission\n  gets `403`, also for an `Idempotency-Key` it used before. Amounts the caller\n  may no longer see are missing from the replay: on quotes they are `null`, on\n  projects and customer requests the field is left out. A project the caller\n  can no longer see answers `404`.\n- Only successful responses (HTTP 2xx) are stored. The request body is not\n  compared, except on `POST /invoices/{invoiceId}/payments`: there the\n  same key with a different payment answers 422 `idempotency_key_reused`.\n  The same applies to partial and final invoices on `POST /invoices`: the\n  same key and source with other `partialInvoice` or `finalInvoice` values or\n  another `invoiceType` answers 422 `idempotency_key_reused`.\n- Stored responses are kept for 30 days after the first request, then\n  deleted. A request after that runs again.\n\n## Rate limits\n\nRequests are limited per organization and day (Berlin calendar day) and per\nAPI key and minute. The limits depend on the plan:\n\n| Access | Requests per day and organization | Requests per minute and key |\n|---|---|---|\n| Business plan (included) | 10,000 | 120 |\n| \"Interface\" add-on (Starter/Premium) | 5,000 | 60 |\n| Practice account | 2,000 | 60 |\n\nThe owner can book the quota package \"+10,000 requests\" in craftivo (one-time\npurchase, not available for practice accounts). Each package raises the\ndaily quota by 10,000 from the moment of payment until the end of the\ncalendar month (Europe/Berlin); several packages add up. `RateLimit-Limit`\nshows the raised quota.\n\nEvery response to a recognized API key carries `RateLimit-Limit`,\n`RateLimit-Remaining` and `RateLimit-Reset` (seconds). They describe the\norganization's daily quota. They describe the key's per-minute limit\ninstead on a 429 caused by that limit (`RateLimit-Remaining: 0`, reset at\nthe next minute) and when no organization with API access is known for the\nrequest (legacy key without `X-Organization-Id`, plan without API).\nExceeding a limit returns HTTP 429 (`rate_limited`) with a `Retry-After`\nheader in seconds: until the next minute for the key limit, until midnight\n(Europe/Berlin) for the daily quota. Requests rejected before the quota\n(`404`, `405`, `insufficient_scope`, paused key) do not count against the\ndaily quota. The limits are exact, also for parallel requests; under very\nheavy parallel load of one organization a request can get a 429 with\n`Retry-After: 1`.\n\nFile uploads (`POST /api/v1/files`) also have their own quota of 300\nuploads per organization and UTC day. A 429 from that quota carries\n`details.quota: \"file_uploads\"` and `Retry-After`, but no `RateLimit-*`\nheaders; those always describe the request quota above.\n\n## Versioning and deprecation\n\nv1 never breaks. A field, endpoint or enum value is never given another\ntype, and no request gets a new required field. A field, endpoint or\nparameter is removed only after it was deprecated and its `x-sunset` has\npassed. Enum values have no deprecation marker, so none is removed. New\noptional fields, new endpoints and new enum values can appear at any time:\nignore unknown fields and handle unknown enum values. A breaking change gets a new\nendpoint or a new version under `/api/v2`. Every contract change is listed\nin the API changelog (`/developers/changelog`).\n\nA deprecated endpoint, parameter or field is marked `deprecated: true` with\n`x-deprecated-at` and `x-sunset` (`YYYY-MM-DD`). It keeps working in v1\nat least until `x-sunset`, which lies at least six months after\n`x-deprecated-at`. From that date it may be removed from v1; each removal\nis listed in the API changelog.\nEvery response of an endpoint that is deprecated or has a deprecated\nparameter or field carries `Deprecation` (RFC 9745, `@<unix seconds>`).\n`Sunset` (RFC 8594, HTTP date) means the whole endpoint may go away: it is\nsent only when the endpoint itself is deprecated, with the endpoint's\n`x-sunset`. A deprecated parameter or field gets no `Sunset` header; its\n`x-sunset` stands in the specification.\n\n## Webhook deliveries\n\ncraftivo `POST`s the event to the endpoint URL as JSON\n(`{ \"id\", \"event\", \"createdAt\", \"data\" }`) with these headers:\n\n- `X-Webhook-Event` — the event name, e.g. `customer.created`.\n- `X-Webhook-Delivery` — the delivery ID, stable across retries.\n- `X-Webhook-Timestamp` — unix seconds.\n- `X-Webhook-Signature` — hex HMAC-SHA256 of `<timestamp>.<raw body>`,\n  keyed with the endpoint secret. Compare it in constant time.\n- `Deprecation` — only when the event or a field of its payload is\n  deprecated in its entry under `webhooks` (RFC 9745,\n  `@<unix seconds>`), as for API responses.\n- `Sunset` — only when the whole event is deprecated and may go away\n  (RFC 8594, HTTP date of the event's `x-sunset`). A deprecated payload\n  field gets no `Sunset` header.\n\nAnswer within 10 seconds with any 2xx status. Everything else is retried up\nto 5 times with growing delays (1 min, 5 min, 15 min, 1 h, 6 h). After 10\nfinal failures in a row the endpoint is disabled automatically; answering\nwith HTTP 410 disables it immediately (use this to unsubscribe). Disabled\nendpoints receive no further deliveries until they are re-activated.\n"},"servers":[{"url":"https://quick-goldfinch-475.eu-west-1.convex.site","description":"Production API host"}],"webhooks":{"customer_request_created":{"post":{"operationId":"receiveCustomerRequestCreatedWebhook","summary":"customer_request.created","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\nOnly reaches an endpoint whose creator can read the request through\n`GET /api/v1/customer-requests/{customerRequestId}`; other endpoints\nget no delivery and no log entry.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["customer_request.created"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/CustomerRequest"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"customer_request_updated":{"post":{"operationId":"receiveCustomerRequestUpdatedWebhook","summary":"customer_request.updated","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\nOnly reaches an endpoint whose creator can read the request through\n`GET /api/v1/customer-requests/{customerRequestId}`; other endpoints\nget no delivery and no log entry.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["customer_request.updated"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/CustomerRequest"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"customer_request_converted":{"post":{"operationId":"receiveCustomerRequestConvertedWebhook","summary":"customer_request.converted","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\nOnly reaches an endpoint whose creator can read the request through\n`GET /api/v1/customer-requests/{customerRequestId}`; other endpoints\nget no delivery and no log entry.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["customer_request.converted"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/CustomerRequest"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"customer_request_archived":{"post":{"operationId":"receiveCustomerRequestArchivedWebhook","summary":"customer_request.archived","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\nOnly reaches an endpoint whose creator can read the request through\n`GET /api/v1/customer-requests/{customerRequestId}`; other endpoints\nget no delivery and no log entry.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["customer_request.archived"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/CustomerRequest"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"member_created":{"post":{"operationId":"receiveMemberCreatedWebhook","summary":"member.created","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["member.created"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/Member"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"member_updated":{"post":{"operationId":"receiveMemberUpdatedWebhook","summary":"member.updated","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["member.updated"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/Member"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"member_deactivated":{"post":{"operationId":"receiveMemberDeactivatedWebhook","summary":"member.deactivated","description":"Carries the full public record, computed after the successful transaction.\nChanges may be coalesced before dispatch. Delivery is asynchronous.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["member.deactivated"]},"createdAt":{"type":"number"},"data":{"$ref":"#/components/schemas/Member"}}}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"documentEvent":{"post":{"operationId":"receiveDocumentWebhook","summary":"Document lifecycle event","description":"Sent to each active endpoint whose subscription matches the event.\nThe concrete target URL is configured through `/api/v1/webhooks`.\nOnly a webhook whose creator may read the document with its `GET`\nroute, for example `GET /api/v1/invoices/{invoiceId}`, gets the event:\nwith the `projects:full` permission every document, otherwise only one\non a project with a team row of the creator and never on a project\nmarked office only. A document without a project, or on a deleted\nproject, needs `projects:full`. Other webhooks get neither the event\nnor an entry in their delivery log.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"formSubmissionCreated":{"post":{"operationId":"receiveFormSubmissionCreatedWebhook","summary":"New non-spam form submission","description":"Sent for submissions created through the website or this API.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormSubmissionWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"formInstanceCreated":{"post":{"operationId":"receiveFormInstanceCreatedWebhook","summary":"New internal form instance","description":"Sent when an internal form instance is created in craftivo or through this API.\nFor a form instance on a project, only a webhook whose creator may read\nthe project with `GET /api/v1/projects/{projectId}` gets this event;\nother webhooks get neither the event nor an entry in their delivery log.\nA form instance on a customer request also needs a request that\n`GET /api/v1/customer-requests/{customerRequestId}` shows to the\nwebhook's creator, as in `GET /api/v1/form-instances`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormInstanceCreatedWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"formInstanceSubmitted":{"post":{"operationId":"receiveFormInstanceSubmittedWebhook","summary":"Submitted internal form instance","description":"Sent for submissions from employees, share links, maintenance protocols, and this API.\nThe project and customer request rules of `form_instance.created` apply\nhere too.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormInstanceSubmittedWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"timeEntryCreated":{"post":{"operationId":"receiveTimeEntryCreatedWebhook","summary":"Created time entry","description":"Sent once for each time entry as soon as it has an end: when it is\nrecorded in craftivo, also for a team or as the correction of a locked\nentry, when it is created through this API, and when a running timer is\nstopped by hand or automatically. A running timer sends nothing. An\nentry that is reopened and stopped again sends `time_entry.updated`,\nnot a second `time_entry.created`. An endpoint whose creator may not see\nthe entry, or may not read its project with\n`GET /api/v1/projects/{projectId}`, only gets `timeEntry.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryCreatedWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"timeEntryUpdated":{"post":{"operationId":"receiveTimeEntryUpdatedWebhook","summary":"Changed time entry","description":"Sent when times, break, category, project or note of a finished time\nentry change, in craftivo or through\n`PATCH /api/v1/time-entries/{entryId}`. Approving, rejecting or\nsubmitting does not send it. An endpoint whose creator may not see the\nentry, or may not read its project with\n`GET /api/v1/projects/{projectId}`, only gets `timeEntry.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryChangeWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"timeEntryDeleted":{"post":{"operationId":"receiveTimeEntryDeletedWebhook","summary":"Deleted time entry","description":"Sent when a finished time entry is deleted, in craftivo or through\n`DELETE /api/v1/time-entries/{entryId}`. `timeEntry` shows the entry as\nit was before. An endpoint whose creator may not see the entry, or may\nnot read its project with `GET /api/v1/projects/{projectId}`, only gets\n`timeEntry.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryChangeWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceVisitScheduled":{"post":{"operationId":"receiveMaintenanceVisitScheduledWebhook","summary":"Scheduled maintenance visit","description":"Sent when a maintenance visit gets its date and people, in craftivo or\nthrough `POST /api/v1/maintenance-contracts/{contractId}/schedule-visits`.\nAn endpoint whose creator lacks the maintenance permission only gets\n`maintenanceVisit.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceContractCreated":{"post":{"operationId":"receiveMaintenanceContractCreatedWebhook","summary":"Created maintenance contract","description":"Sent for a contract created in craftivo or through\n`POST /api/v1/maintenance-contracts`. Prices are left out when the endpoint's\ncreator lacks the maintenance permission `full`. Without any maintenance\npermission the endpoint only gets `maintenanceContract.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceContractUpdated":{"post":{"operationId":"receiveMaintenanceContractUpdatedWebhook","summary":"Changed maintenance contract","description":"Sent when a contract or its status changes, in craftivo or through\n`PATCH /api/v1/maintenance-contracts/{contractId}` and\n`POST /api/v1/maintenance-contracts/{contractId}/status`. Sending the status\nthe contract already has sends nothing. Prices are redacted as for\n`maintenance_contract.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceContractArchived":{"post":{"operationId":"receiveMaintenanceContractArchivedWebhook","summary":"Archived maintenance contract","description":"Sent when a contract moves to the archive, in craftivo or through\n`POST /api/v1/maintenance-contracts/{contractId}/archive`. Archiving it again\nsends nothing. Prices are redacted as for `maintenance_contract.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceContractRestored":{"post":{"operationId":"receiveMaintenanceContractRestoredWebhook","summary":"Restored maintenance contract","description":"Sent when a contract returns from the archive, in craftivo or through\n`POST /api/v1/maintenance-contracts/{contractId}/restore`. Restoring it again\nsends nothing. Prices are redacted as for `maintenance_contract.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"maintenanceVisitCompleted":{"post":{"operationId":"receiveMaintenanceVisitCompletedWebhook","summary":"Completed maintenance visit","description":"Sent when a maintenance visit is completed: by the office, by the\ntechnician in the mobile app, by turning it into an order, or through\n`POST /api/v1/maintenance-visits/{visitId}/complete`. Completing it again\nsends nothing. An endpoint whose creator lacks the maintenance permission\nonly gets `maintenanceVisit.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"scheduleEntryCreated":{"post":{"operationId":"receiveScheduleEntryCreatedWebhook","summary":"Created assignment","description":"Sent for an assignment created in the planning board of craftivo or\nthrough `POST /api/v1/schedule-entries`, for a scheduled maintenance\nvisit, when a project is created with a first assignment, when a\ntechnician takes over open work, and for an appointment from a block of\na service plan. Releasing a pre-planning or a project from the queue\nsends one event for each assignment it creates, in batches of 50 about\none second apart. A pre-planning bar is not an assignment and sends\nnothing. An endpoint whose creator lacks the planning permission, or\nmay not read the project with `GET /api/v1/projects/{projectId}`, only\ngets `scheduleEntry.id`. The same applies to `schedule_entry.updated`\nand `schedule_entry.deleted`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"scheduleEntryUpdated":{"post":{"operationId":"receiveScheduleEntryUpdatedWebhook","summary":"Changed assignment","description":"Sent when an assignment moves, changes its members, team, notes or\nstatus, in craftivo or through\n`PATCH /api/v1/schedule-entries/{entryId}`. A request that changes\nfields and the status sends two events. A changed date of a recurring\nassignment carries `recurrenceId`, also when a cancelled date is\nrestored. Also sent when a technician takes over a scheduled maintenance\nvisit and when a maintenance appointment moves. Changes to many\nassignments at once send one event for each of them, in batches of 50\nabout one second apart: moving the start of their project, taking a\nmember off a project together with their assignments, and deactivating\nor removing a member. These are computed when sent, so they can already\nshow later changes, and an assignment deleted in the meantime only sends\n`schedule_entry.deleted`. Team notes, changing the members of a team and\nturning a maintenance visit into an order send no event yet.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"scheduleEntryDeleted":{"post":{"operationId":"receiveScheduleEntryDeletedWebhook","summary":"Deleted assignment","description":"Sent when an assignment or a single date of a recurring assignment is\ndeleted, in craftivo or through\n`DELETE /api/v1/schedule-entries/{entryId}`. `scheduleEntry` is the last\nstate; a deleted date carries `recurrenceId` and status `cancelled`.\nAssignments that go with something else send one event each, in batches\nof 50 about one second apart: those released from a deleted pre-planning,\nthose of the open visits of an archived maintenance contract, and those\nof a deleted project once its undo period has ended. For a deleted\nproject an endpoint only gets `scheduleEntry.id`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"paymentCreated":{"post":{"operationId":"receivePaymentCreatedWebhook","summary":"Recorded payment","description":"Sent for every payment or correction recorded on an invoice, in\ncraftivo or through `POST /api/v1/invoices/{invoiceId}/payments`.\n`invoice` carries the payment state after the booking. The invoice\nevents (`invoice.updated`, and `invoice.partially_paid` or\n`invoice.paid` when the status changes) are sent as well. A repeated\nrequest with the same `externalId` sends nothing. Only a webhook whose\ncreator may read the invoice with `GET /api/v1/invoices/{invoiceId}`\ngets the event, as for the invoice events.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentCreatedWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"projectCreated":{"post":{"operationId":"receiveProjectCreatedWebhook","summary":"project.created","description":"Sent once for every new project or order, whichever way it was\ncreated: in craftivo, through `POST /api/v1/projects`, by converting a\ncustomer request, or as the hidden draft project of a customer\nrequest in craftivo (then with `visibleOnBoard: false`). Taking over a\ndraft project later sends no second `project.created`. Projects\nimported from another program send nothing.\n`data.project` is the public project as `GET /api/v1/projects/{projectId}`\nreturns it, computed after the successful transaction.\n`estimatedBudget` and `actualCost` are only included when the member\nwho created the webhook may see financials.\nThe event only reaches a webhook whose creator may read the project\nwith `GET /api/v1/projects/{projectId}`: with the full projects\npermission every project, otherwise only with a team row and never a\nproject marked office-only. Being scheduled on the project is not\nenough, and a creator who left the organization gets nothing. Other\nwebhooks get neither this event nor an entry in their delivery log.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"projectUpdated":{"post":{"operationId":"receiveProjectUpdatedWebhook","summary":"project.updated","description":"Sent when a project or order is edited in craftivo (project details\nand custom fields), when a customer request adds its customer or\nproperty to the project or is converted into it, and for\n`PATCH`/`PUT /api/v1/projects/{projectId}`. Also sent when a project\nor order is archived or restored, completed, reopened for rework,\nmoved to another order status on the board, converted from project to\norder or back, and when its pre-planning bar on the schedule sets new\ndates. Completing an order that is already completed sends nothing.\nDeleting a status column that still holds projects or orders moves\nthem to the chosen column and sends one event for each of them, also\nfor deleted ones. A large column sends these in batches of 50 about\none second apart, so they can arrive some time after the column is\ngone. `archivedAt` tells archiving from editing: it holds the time\nafter archiving and is `null` after restoring. A deleted project\ncoming back also sends this event: the undo after deleting, reopening\na lost customer request whose draft project went with it, and using\nthe deleted draft project of a customer request again. Deleting a\nproject sends `project.deleted` instead. Same payload as\n`project.created`, computed after the successful transaction, so it\ncan already show later changes. The read rule of `project.created`\napplies here too.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"projectDeleted":{"post":{"operationId":"receiveProjectDeletedWebhook","summary":"project.deleted","description":"Sent once when a project or order is deleted in craftivo, and when a\ncustomer request is marked as lost and its draft project is deleted\nwith it. Deleting it again sends nothing. The project only carries a\ndeletion mark: `GET /api/v1/projects` no longer lists it and\n`GET /api/v1/projects/{projectId}` answers 404 `not_found`. If it comes\nback, you get `project.updated`. Undoing an import of another program\nsends nothing. Same payload as `project.created`, showing the project\nas it was when deleted, so with `visibleOnBoard: false`. The read rule\nof `project.created` applies here too.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"propertyCreated":{"post":{"operationId":"receivePropertyCreatedWebhook","summary":"property.created","description":"Sent once for every new property (Objekt), created in craftivo or\nthrough `POST /api/v1/properties`. `data.property` is the public\nproperty as `GET /api/v1/properties` lists it. Adding a party alone\nsends no property event.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"propertyUpdated":{"post":{"operationId":"receivePropertyUpdatedWebhook","summary":"property.updated","description":"Sent when a field of the public property changes, in craftivo or\nthrough `PATCH /api/v1/properties/{propertyId}`, also when only its\ndefault parties change. Changing only the contact person or the on-site\ncontacts sends nothing. Same payload as `property.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"propertyDeleted":{"post":{"operationId":"receivePropertyDeletedWebhook","summary":"property.deleted","description":"Sent when a property is deleted, in craftivo or through\n`DELETE /api/v1/properties/{propertyId}`. The property only carries a\ndeletion mark; its projects, documents and parties stay. Same payload\nas `property.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}},"propertyRestored":{"post":{"operationId":"receivePropertyRestoredWebhook","summary":"property.restored","description":"Sent when a deleted property comes back, in craftivo or through\n`POST /api/v1/properties/{propertyId}/restore`. Same payload as\n`property.created`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyWebhookDelivery"}}}},"responses":{"200":{"description":"Any 2xx response acknowledges the delivery."}}}}},"security":[{"ApiKeyAuth":[]}],"tags":[{"name":"Account","description":"Discovery of the authenticated API user."},{"name":"Customers","description":"Customer records of an organization."},{"name":"Customer requests","description":"Incoming customer requests, Kanban placement, and draft quotes created from a request."},{"name":"Properties","description":"Properties (Objekte, buildings of customers) and their parties\n(Hausverwaltung, WEG, owners, tenants …). A property belongs to the\norganization, not to one customer: customers are linked through parties.\nEvery member can read; creating and changing needs the projects\npermission (level `own` is enough). A property deleted in craftivo is not\nlisted and returns 410 `property_deleted`, also for its parties. Deleting\nand restoring are not part of the API.\n"},{"name":"Maintenance","description":"Read maintenance contracts (Wartungsverträge) with their equipment and the\nmaintenance visits (Wartungstermine). Reading requires the maintenance\npermission at level `view` or `full`; without it every call returns 403.\nPrices, the billing mode, catalog positions and invoice drafts are only\nreturned to members with `full`. Statuses keep the values of the app:\ncontracts `aktiv`, `pausiert`, `beendet`; visits `offen`, `eingeplant`,\n`erledigt`.\n\nScheduling and completing a visit needs a read-write key and the\nmaintenance permission `full` of the key owner, like in the web app. A\ntechnician assigned to a visit closes it in the mobile app with protocol\nand acceptance, not through this API. Photos are not part of the API.\n\nCreating, changing, archiving and restoring contracts also needs a\nread-write key and the maintenance permission `full`. A contract carries\nits maintenance type as `templateId` from `GET /api/v1/maintenance-templates`.\nAssigning forms by hand and offer takeover stay in craftivo.\n"},{"name":"Members","description":"Active organization members that can be assigned to tasks."},{"name":"Positions","description":"Read-only access to the organization's position library, for building quotes from it."},{"name":"Absences","description":"Absences of members (vacation, sick leave and more), company vacations\n(Betriebsferien) and public holidays of the organization, so a client\nknows who is away before it plans an assignment. Members with the time\ntracking permission at level `view` or higher see every member, all\nothers only themselves. The type `sick` is only shown to members with the\nemployees permission at level `full`; everyone else sees `absent`\ninstead. Notes, rejection reasons and vacation balances are not part of\nthe API. Read-only.\n"},{"name":"Schedule","description":"Read the planning board (Plantafel): assignments (Einsätze), the rough\nproject periods of the pre-planning (Vorplanung) and the dates of\nrecurring assignments. Members with the planning permission see the whole\nboard like in the web app. All other members see what their mobile app\nshows: their own assignments with status `planned`, `confirmed`,\n`in_progress` or `paused`, from Monday of the current week to Sunday of\nthe 13th week.\n\nWriting needs a read-write key and the planning permission of the key\nowner; a member with only \"add project to board\" may create. The rules\nof the planning board apply: conflict detection, cascades and\nnotifications to the members. Recurring assignments, pre-planning and\nrelease stay in the planning board (`use_web_planner`).\n"},{"name":"Tasks","description":"Tasks, calls, appointments, measurements, emails, and reminders."},{"name":"Projects","description":"Projects and orders, including their members."},{"name":"Teams","description":"Crews (Kolonnen) of an organization with their members. Member ids are\nthe same ids as in `GET /api/v1/members`, task `assigneeId` and time\nentry `userId`; members that `GET /api/v1/members` does not list are\nleft out. Read-only.\n"},{"name":"Time entries","description":"Time entries of members on projects or orders (CI-3052). Entries are\ncreated in `draft` status and follow the same rules as the app: the API\nkey's member must be allowed to book time for the target member, and\noverlapping entries are rejected unless `allowOverlap` is set.\n"},{"name":"Files","description":"File uploads for form submissions and customer requests. Upload the raw\nfile to `POST /api/v1/files`, then reference the returned file id in a\nform submission answer or attach it to a customer request. Each id is\nusable exactly once; unused uploads expire after 24 hours.\n"},{"name":"Forms","description":"External forms plus internal form templates, instances, and submissions."},{"name":"Documents","description":"Read-only access to quotes, estimates, confirmations, delivery notes, and invoices.\n\nA key sees a document only when it may read its project with\n`GET /api/v1/projects/{projectId}`: with the `projects:full` permission\nevery project, otherwise only with a team row and never a project marked\noffice only. Being scheduled on the project is not enough. A document\nwithout a project, or on a deleted project, needs `projects:full`. Any\nother document answers 404 `not_found` like an unknown one and is missing\nfrom the lists. The same rule applies to payments, finalize, send,\naccept, reject, draft changes, new quotes and invoices, and to repeated\ncalls with the same `Idempotency-Key`.\n"},{"name":"Sandbox","description":"Reset a practice account (Übungskonto) to its sample data: customers\nwith umlauts, a multi-family house with its property management, customer\nrequests in several board columns, quotes, an invoice with a partial\npayment, a crew with time entries, and planning board assignments. Only\nsandbox organizations accept these routes.\n"},{"name":"Webhooks","description":"Self-service webhook subscriptions of an integration. craftivo stores UI\nand API endpoints in one place; this API only sees its own\n(`source: api`).\n"}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API key from Settings → \"API & Webhooks\"."}},"parameters":{"OrganizationId":{"in":"header","name":"X-Organization-Id","required":false,"description":"Required when the API user belongs to multiple organizations. Keys that\nare bound to one organization only accept that organization.\n","schema":{"type":"string"}},"IdempotencyKey":{"in":"header","name":"Idempotency-Key","required":false,"description":"Replays the stored response of an earlier request with the same key,\nmethod and path. Stored responses are kept for 30 days.\n","schema":{"type":"string"}},"UpdatedAfter":{"in":"query","name":"updatedAfter","required":false,"description":"Return resources updated at or after this point in time. Accepts an ISO\n8601 timestamp or a unix timestamp in milliseconds.\n","schema":{"type":"string"},"example":"2026-08-01T00:00:00.000Z"},"DocumentStatus":{"in":"query","name":"status","required":false,"description":"Comma-separated document statuses.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["draft","finalized","sent","customer_review","accepted","rejected","superseded","booked","partially_paid","overdue","paid","cancelled","archived"]}}},"DocumentCustomerId":{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},"DocumentProjectId":{"in":"query","name":"projectId","required":false,"schema":{"type":"string"}},"DocumentDateFrom":{"in":"query","name":"dateFrom","required":false,"schema":{"type":"string","format":"date"}},"DocumentDateTo":{"in":"query","name":"dateTo","required":false,"schema":{"type":"string","format":"date"}},"InvoiceType":{"in":"query","name":"type","required":false,"description":"Comma-separated invoice types.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["standard","partial","final","down_payment","credit_note"]}}},"Limit":{"in":"query","name":"limit","required":false,"description":"Page size, clamped server-side to 1…100.","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"Cursor":{"in":"query","name":"cursor","required":false,"description":"Pagination cursor from `meta.cursor` of the previous page.","schema":{"type":"string"}},"ExternalIdFilter":{"in":"query","name":"externalId","required":false,"description":"Exact match on the external ID of the connected system.","schema":{"type":"string"}}},"headers":{"IdempotencyReplayed":{"description":"Present when a stored response was replayed.","schema":{"type":"string","enum":["true"]}},"RetryAfter":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":1}},"RateLimitLimit":{"description":"Requests the organization may make per day (Berlin calendar day). The\nper-minute limit of the key instead on a 429 caused by that limit and\nwhen no organization with API access is known for the request (legacy\nkey without `X-Organization-Id`, plan without API).\n","schema":{"type":"integer","minimum":0}},"RateLimitRemaining":{"description":"Requests left in the current window after this request.","schema":{"type":"integer","minimum":0}},"RateLimitReset":{"description":"Seconds until the current window resets (midnight Europe/Berlin for the daily quota, the next minute for the per-minute limit).","schema":{"type":"integer","minimum":1}}},"responses":{"BadRequest":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Validation error — `invalid_body`, `invalid_json`, `invalid_filter`,\n`invalid_customer`, `invalid_property`, `invalid_template`,\n`external_id_mismatch` or `organization_required`.\n`POST /invoices` also answers `idempotency_key_required` without an\n`Idempotency-Key` header and `unsupported_invoice_type` for an\n`invoiceType` other than `standard`, `partial`, `down_payment` or `final`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"Unauthorized":{"description":"Missing or invalid API key (`unauthorized`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"PaymentRequired":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The organization's trial has ended, its payment is overdue or its API\nadd-on has ended (`billing_access_required`). Writes are rejected until a\nsubscription or the add-on is active again; `details.accessState` names\nthe billing state, `details.feature: api_access` an ended add-on. Reads\nand `DELETE /api/v1/webhooks/{webhookId}` keep working.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"Forbidden":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"`api_key_paused` (the owner paused this key), `insufficient_scope`\n(read-only key or missing extra right; `details.requiredScope` names\nwhat is missing), `plan_feature_required` (plan without API access\nand without the API add-on, or without a plan feature the route needs;\n`details.feature` names it) or\n`forbidden` (organization not accessible, or a permission of the key's\ncreator is missing; when the missing permission is known,\n`details.requiredPermission` names it in the notation of `permissions`\nin `GET /api/v1/me`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"SandboxForbidden":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"`not_a_sandbox` (the organization is no practice account and keeps its data),\n`forbidden` (the key's creator is not the owner;\n`details.requiredPermission` is `owner`) or one of the general\n`403` codes (`api_key_paused`, `insufficient_scope`,\n`plan_feature_required`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"SandboxResetConflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"A reset is already running (`reset_in_progress`). Poll `GET /api/v1/sandbox/reset`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"NotFound":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Unknown route or resource (`not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"PropertyDeleted":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The property was deleted in craftivo (`property_deleted`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"MethodNotAllowed":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Known path called with an unsupported method (`method_not_allowed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"Conflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"State conflict — a duplicate `externalId` (`conflict`), a referenced\nfile consumed by a parallel request (`conflict`), the webhook limit of\nan organization (`webhook_limit_reached`), the attachment limit of a\ntask (`attachment_limit_reached`), a test delivery for a\ndisabled webhook (`endpoint_disabled`), an `articleNumber` matching\nseveral positions (`ambiguous_article_number`), a customer request\nthat was already converted (`already_converted`, with\n`details.convertedProjectId`) or a quote that can no longer be changed\n(`document_finalized`, `document_locked`, `version_conflict`). When\nfinalizing: the document is no longer a draft (`document_finalized`),\nsomeone is editing it in craftivo (`document_locked`) or a rule for\npartial and final invoices blocks it (`conflict`). When cancelling an\ninvoice: it has booked payments (`invoice_has_payments`) or its state\ndoes not allow it (`cancellation_conflict`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"PaymentConflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Nothing was booked. Codes: `payment_exceeds_open_amount` (amount plus\nskonto exceed the open amount), `external_id_conflict` (this invoice\nalready has a payment with this `externalId` and different data) and\n`payment_not_allowed` with `details.reason`: `invoice_not_finalized`,\n`status_unsupported` (e.g. cancelled), `document_type_unsupported`\n(credit note), `receivable_superseded` (a down payment replaced by the\nfinal invoice), `legacy_import_read_only` (down payment imported from\nearlier software) or `paid_without_payment_record` (marked as paid in\ncraftivo, but its recorded payments do not cover the amount; nothing\nis open, so no further payment or positive correction is booked).\nA correction also answers `settlement_negative` when it would make the\npaid amount of the invoice negative.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"FinalizationIncomplete":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The document lacks information required for finalizing\n(`finalization_incomplete`). `details.missingFields` lists every\nfinding in the order craftivo checks them; the first one is the error\ncraftivo shows in the app. Messages are German, as in craftivo.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinalizationIncompleteError"}}}},"SendConflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"description":"The document cannot be sent now; no email went out. Codes:\n`document_not_finalized` (a draft; finalize it first, craftivo never\nfinalizes it on its own), `document_not_sendable` (cancelled,\nsuperseded or another status that is not sent), `document_sender_unavailable`\n(no usable sender mailbox), `pdf_not_ready` (archive PDF not ready,\nwith `Retry-After`), `document_already_sent` (pass `resend: true`) and\n`send_outcome_unknown` (another send of this document runs, or an\nearlier one has no known outcome; never retried automatically).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"SendUnprocessable":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Missing input for the email: neither `templateId` nor `subject` and\n`body` (`message_required`), an unknown or inaccessible template\n(`template_not_found`) or no delivery address on the document and no\n`to` (`recipient_email_missing`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"StatusConflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The document's status does not allow the request: still a draft\n(`document_not_finalized`), finalized but not sent yet\n(`document_not_sent`), the customer already answered the other way\nor the document is superseded or archived\n(`invalid_status_transition`), or the quote cannot become an order\nconfirmation (`document_not_convertible`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"InvoiceConflict":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The invoice draft cannot be created or changed. Creating: the source\nis a draft, rejected, archived, cancelled or superseded\n(`invalid_source_status`), an addendum or part of a collective\ninvoice (`source_not_invoiceable`), or a maintenance visit that cannot\nbe billed (`visit_not_billable`, reason in `details.reason`, for\nexample `recipient_party_inactive`), or a partial invoice that a lock\nof its order forbids (`partial_invoice_conflict`, with the reason in\nGerman in `message`), or a final invoice that a lock forbids\n(`final_invoice_conflict`, with the reason in German in `message`).\nChanging: the invoice is no longer\na draft (`document_finalized`), someone is editing it in craftivo\n(`document_locked`), `version` is outdated (`version_conflict`) or it\nis not a standard invoice (`unsupported_invoice_type`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"CustomerRequired":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The quote or source document has no customer (`customer_required`),\nor its recipient party belongs to another property than the project\n(`recipient_party_not_on_property`). Fix it in craftivo first.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"InvoiceCreateUnprocessable":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The quote or source document has no customer (`customer_required`),\nits recipient party belongs to another property than the project\n(`recipient_party_not_on_property`), or the `Idempotency-Key` was\nalready used with other values for this source\n(`idempotency_key_reused`).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"IdempotencyKeyReused":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"The `Idempotency-Key` was already used for a different payment on this invoice (`idempotency_key_reused`). Nothing was booked. Use a new key for a new payment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"SendFailed":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"`send_failed`: the mail server rejected the email, nothing was sent,\na retry is safe. `send_outcome_unknown`: craftivo cannot tell whether\nthe email went out; check the document before you send again. A retry\nwith the same `Idempotency-Key` answers 409 and never sends twice.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"PayloadTooLarge":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"File larger than 20 MB, or a document larger than 10 MB attached to a task (`file_too_large`), or the 1 GB storage of a practice account is full (`storage_limit_reached`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"UnsupportedMediaType":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Content type outside the allowlist (`unsupported_file_type`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"RateLimited":{"description":"Rate limit exceeded (`rate_limited`).","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"InternalError":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Unexpected server error (`internal_error`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}}},"schemas":{"ApiError":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","examples":["invalid_body"]},"message":{"type":"string","examples":["startDate must use YYYY-MM-DD format."]},"details":{"type":"object","additionalProperties":true}}}}},"DocumentRejectRequest":{"type":"object","additionalProperties":false,"required":["rejectionReason"],"properties":{"rejectionReason":{"type":"string","enum":["customer_chose_other","price_expectation_not_met","project_postponed","project_not_implemented","no_response","superseded_by_new_offer","other"],"description":"Reason from the fixed list in craftivo."},"rejectionComment":{"type":"string","maxLength":2000,"description":"Optional note, shown in the document history."}}},"DocumentSendRequest":{"type":"object","additionalProperties":false,"description":"Either `templateId` or `subject` and `body`. craftivo never writes the\ntext itself.\n","properties":{"templateId":{"type":"string","description":"Email template of the organization (team template or a personal template of the key's creator). Placeholders are filled from the document, its project and customer."},"subject":{"type":"string","maxLength":998},"body":{"type":"string","maxLength":100000,"description":"Plain text. Line breaks are kept."},"to":{"type":"array","maxItems":20,"items":{"type":"string","format":"email"},"description":"Replaces the document's delivery address. Leave out to use it."},"cc":{"type":"array","maxItems":20,"items":{"type":"string","format":"email"}},"bcc":{"type":"array","maxItems":20,"items":{"type":"string","format":"email"},"description":"The organization's default BCC is added for quotes, invoices and down payment invoices, as in craftivo."},"dryRun":{"type":"boolean","default":false,"description":"Returns the email without sending it. Changes nothing and is never stored under the `Idempotency-Key`."},"resend":{"type":"boolean","default":false,"description":"Required to send a document that was already sent."}}},"DocumentSendResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["documentId","dryRun","status","sentAt","to","cc","bcc","subject","body","bodyFormat","attachments","sender","pdfReady","sink","unresolvedPlaceholders","warnings"],"properties":{"documentId":{"type":"string"},"dryRun":{"type":"boolean"},"status":{"type":"string","description":"Status of the document after sending.","examples":["sent"]},"sentAt":{"type":["string","null"],"format":"date-time"},"to":{"type":"array","items":{"type":"string"}},"cc":{"type":"array","items":{"type":"string"}},"bcc":{"type":"array","items":{"type":"string"}},"subject":{"type":"string"},"body":{"type":"string"},"bodyFormat":{"type":"string","enum":["text","html"]},"attachments":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["fileName","contentType"],"properties":{"fileName":{"type":"string"},"contentType":{"type":"string"}}}},"sender":{"type":"object","additionalProperties":false,"required":["address","provider","label"],"properties":{"address":{"type":"string"},"provider":{"type":"string","enum":["microsoft","google","imap"]},"label":{"type":"string"}}},"pdfReady":{"type":"boolean"},"sink":{"type":["boolean","null"],"description":"`true` when craftivo did not deliver the email: outside craftivo's production environment, or always for a practice account (Übungskonto). The email is then only in the mailbox history with the status \"Testversand, nicht zugestellt\"; status and webhooks behave as after a real send. `null` for `dryRun`."},"unresolvedPlaceholders":{"type":"array","items":{"type":"string"},"description":"Template placeholders craftivo could not fill. They stay in the text as written."},"warnings":{"type":"array","description":"The email was sent, but a later step failed, e.g. `status_not_updated`. Do not send again.","items":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}}},"FinalizationIncompleteError":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message","details"],"properties":{"code":{"type":"string","const":"finalization_incomplete"},"message":{"type":"string"},"details":{"type":"object","additionalProperties":false,"required":["missingFields"],"properties":{"missingFields":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["code","field","message"],"properties":{"code":{"type":"string","description":"Machine code of the finding, e.g. `INVOICE_FINALIZATION_VALIDATION` or `VALIDATION_ERROR`.","examples":["INVOICE_FINALIZATION_VALIDATION"]},"field":{"type":"string","description":"What is missing, e.g. `recipient`, `positions`, `date`, `organization.addressZip`, `organization.taxNumber`, `organization.addressCountry` or `recipient.vatId`.","examples":["organization.addressZip"]},"message":{"type":"string","examples":["Lieferanten-PLZ fehlt"]}}}}}}}}}},"PaginationMeta":{"type":"object","additionalProperties":false,"required":["cursor","isDone","limit"],"properties":{"cursor":{"type":"string","description":"Pass as `cursor` to fetch the next page."},"isDone":{"type":"boolean"},"limit":{"type":"integer","minimum":1,"maximum":100}}},"OrganizationMembership":{"type":"object","additionalProperties":false,"description":"Memberships carry `joinedAt` instead of the usual `createdAt`/`updatedAt` pair: joining is the only tracked write, so a separate update timestamp would never change.","required":["id","organizationId","organizationName","role","memberType","joinedAt","permissions"],"properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"organizationName":{"type":"string","description":"Display name of the organization. Integrations such as Zapier use this as the connection label."},"role":{"type":"string","enum":["owner","office","site_manager","technician"]},"memberType":{"type":"string","description":"Effective workplace of the member. `field` members use the mobile app on site, `office` members work on the desktop app.","enum":["field","office"]},"joinedAt":{"type":"string","format":"date-time"},"permissions":{"type":"array","description":"Effective permissions of the key's creator in this organization, from role and individual overrides, as in the craftivo app. A yes/no permission appears as its key, a graded permission as `key:level`; permissions not granted and the level `none` are left out. Sorted alphabetically. A 403 `forbidden` names a missing one in `details.requiredPermission` when it is known.","items":{"type":"string"},"examples":[["maintenance:view","projects:own","self_assign_work","time_tracking:own"]]}}},"Me":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","apiKey","organizations"],"properties":{"id":{"type":"string","description":"User ID of the key owner, as used in `assigneeId`/member IDs."},"apiKey":{"type":"object","additionalProperties":false,"description":"Scope and extra rights of the key that made this request.","required":["scope","extraRights"],"properties":{"scope":{"type":"string","enum":["read","read_write"]},"extraRights":{"type":"array","description":"Rights the organization owner enabled for this key. Empty when none are enabled.","items":{"type":"string","enum":["documents:finalize","documents:send","payments:write","payments:correct"]}}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"organizations":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationMembership"}}}},"MeResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Me"}}},"CustomFieldValues":{"type":"object","description":"Custom field values keyed by public field slug.","additionalProperties":true},"Customer":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"customerType":{"type":"string","enum":["business","individual"]},"salutation":{"type":"string","enum":["mr","ms","neutral","married_couple","family"],"description":"`married_couple` is the German \"Eheleute\", `family` is \"Familie\".\nBoth address the household by last name.\n"},"firstName":{"type":"string"},"lastName":{"type":"string"},"company":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"},"taxNumber":{"type":"string"},"notes":{"type":"string"},"customFields":{"$ref":"#/components/schemas/CustomFieldValues"}}},"CustomerInput":{"type":"object","additionalProperties":true,"properties":{"customerType":{"type":"string","enum":["business","individual"]},"salutation":{"type":"string","enum":["mr","ms","neutral","married_couple","family"],"description":"`married_couple` is the German \"Eheleute\", `family` is \"Familie\".\nBoth address the household by last name.\n"},"firstName":{"type":"string"},"lastName":{"type":"string"},"company":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string"},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"country":{"type":"string"},"taxNumber":{"type":"string"},"notes":{"type":"string"},"customFields":{"$ref":"#/components/schemas/CustomFieldValues"}}},"CreateCustomerInput":{"allOf":[{"$ref":"#/components/schemas/CustomerInput"},{"type":"object","required":["firstName","lastName"]}]},"CustomerResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Customer"}}},"CustomerListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Customer"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CustomerRequest":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","title","source","priority","lostAt"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"title":{"type":"string"},"description":{"type":"string"},"source":{"type":"string","enum":["website_form","phone","email","referral","manual"]},"priority":{"type":"string","enum":["low","medium","high"]},"temperature":{"type":"string","enum":["hot","active","waiting","cold"]},"customerId":{"type":"string"},"assignedUserId":{"type":"string"},"address":{"type":"string"},"nextStep":{"type":"string"},"followUpDueAt":{"type":"string","format":"date-time"},"estimatedValue":{"type":"integer","description":"Estimated value in integer cents. Omitted without financials permission."},"currency":{"type":"string","enum":["EUR"],"description":"Present only together with estimatedValue."},"columnId":{"type":"string"},"columnName":{"type":"string"},"linkedProjectId":{"type":"string"},"convertedProjectId":{"type":"string"},"lostAt":{"type":["string","null"],"format":"date-time","description":"Time the request was marked lost. Null after reopening or if never lost."},"archivedAt":{"type":"string","format":"date-time"}}},"CreateCustomerRequestInput":{"type":"object","additionalProperties":false,"required":["title"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"customerId":{"type":"string"},"source":{"type":"string","enum":["website_form","phone","email","referral","manual"]},"priority":{"type":"string","enum":["low","medium","high"]},"estimatedValue":{"type":"integer","minimum":0,"description":"Estimated value in integer cents, stored exactly. A value of 0 is\nstored as absent. Requires financials permission.\n"},"address":{"type":"string"},"columnId":{"type":"string","description":"Target column from GET /customer-request-columns. A column of another\nboard returns 400.\n"},"nextStep":{"type":["string","null"],"description":"Next step as free text. Requires projects permission at level full."},"followUpDueAt":{"type":["string","null"],"format":"date-time","description":"Follow-up date as ISO 8601 timestamp. Only the\nfield is set: it does not record a contact, does not set the last\ncontact time and creates no call task. Requires projects permission at\nlevel full.\n"},"temperature":{"type":["string","null"],"enum":["hot","active","waiting","cold",null],"description":"Requires projects permission at level full."},"assignedUserId":{"type":["string","null"],"description":"Responsible member (user id from GET /members). Defaults to the\ncreator of the API key; null leaves the request unassigned. A user\noutside the organization returns 400. The new request notifies the\nresponsible member in the app, or the office when it is unassigned.\nRequires projects permission at level full.\n"}}},"PatchCustomerRequestInput":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string"},"description":{"type":"string"},"customerId":{"type":["string","null"],"description":"Set to null to clear the linked customer."},"source":{"type":"string","enum":["website_form","phone","email","referral","manual"]},"priority":{"type":"string","enum":["low","medium","high"]},"estimatedValue":{"type":["integer","null"],"minimum":0,"description":"Integer cents, stored exactly. 0 or null clears the value.\nRequires financials permission.\n"},"address":{"type":"string"},"columnId":{"type":"string","description":"Moves the Kanban card to this column from GET /customer-request-columns.\nA column of another board returns 400; null is rejected. Moving to a\n`won` column does not create a project and moving to a `lost` column\nsets no loss reason; use the conversion of the request for that.\n"},"nextStep":{"type":["string","null"],"description":"Next step as free text. Null clears it. Requires projects permission at level full."},"followUpDueAt":{"type":["string","null"],"format":"date-time","description":"Follow-up date as ISO 8601 timestamp. Null\nclears it. Only the field changes: it does not record a contact, does\nnot set the last contact time, does not count as \"not reached\" and\ncreates no call task. Requires projects permission at level full.\n"},"temperature":{"type":["string","null"],"enum":["hot","active","waiting","cold",null],"description":"Null clears it. Requires projects permission at level full."},"assignedUserId":{"type":["string","null"],"description":"Responsible member (user id from GET /members). Null removes the\nresponsible member. A user outside the organization returns 400. The\nchange appears in the request history and notifies the new member.\nRequires projects permission at level full.\n"}}},"CustomerRequestColumn":{"type":"object","additionalProperties":false,"required":["id","name","order","kind"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number","description":"Sort ascending to get the board order."},"kind":{"type":"string","enum":["open","won","lost"]}}},"CustomerRequestColumnListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomerRequestColumn"}}}},"ProjectStatus":{"type":"object","additionalProperties":false,"required":["id","name","order","systemKey","isSystem"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"order":{"type":"number","description":"Sort ascending to get the order shown in craftivo."},"systemKey":{"type":["string","null"],"enum":["planning","completed","cancelled",null],"description":"Stable key of a system status. Null for statuses the organization created."},"isSystem":{"type":"boolean","description":"System statuses cannot be deleted in craftivo."}}},"ProjectStatusListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ProjectStatus"}}}},"CreateCustomerRequestOfferInput":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string"}}},"ConvertCustomerRequestInput":{"type":"object","additionalProperties":false,"properties":{"target":{"type":"string","enum":["project","order"],"default":"project","description":"`project` creates a draft project on the project board, `order`\na running order on the order board.\nOmit it for `project`. `null`, an empty string and unknown\nfields return 400 and convert nothing.\n"}}},"CustomerRequestResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CustomerRequest"}}},"CustomerRequestListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CustomerRequest"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CustomerRequestQuoteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Quote"}}},"CustomerRequestDeleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","archived","archivedAt"],"properties":{"id":{"type":"string"},"archived":{"type":"boolean","enum":[true]},"archivedAt":{"type":"string","format":"date-time"}}}}},"ApiFile":{"type":"object","additionalProperties":false,"required":["id","fileName","mimeType","fileSize","createdAt","expiresAt"],"properties":{"id":{"type":"string","description":"File id to reference in a form submission or customer request. Usable exactly once."},"fileName":{"type":"string"},"mimeType":{"type":"string"},"fileSize":{"type":"number","description":"Size in bytes."},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time","description":"Unused uploads are deleted after this point in time."}}},"ApiFileResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ApiFile"}}},"AttachCustomerRequestFileInput":{"type":"object","additionalProperties":false,"required":["fileId"],"properties":{"fileId":{"type":"string","minLength":1,"description":"File id from `POST /api/v1/files`."}}},"CustomerRequestFileAttachment":{"type":"object","additionalProperties":false,"required":["id","customerRequestId","fileName","mimeType","fileSize","uploadedAt"],"properties":{"id":{"type":"string"},"customerRequestId":{"type":"string"},"fileName":{"type":"string"},"mimeType":{"type":"string"},"fileSize":{"type":"number","description":"Size in bytes."},"uploadedAt":{"type":"string","format":"date-time"}}},"CustomerRequestFileAttachmentResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CustomerRequestFileAttachment"}}},"Project":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","status","type","currency","customerRequestId","visibleOnBoard"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"customerRequestId":{"type":["string","null"],"description":"Originating customer request, or null for independently created projects."},"visibleOnBoard":{"type":"boolean","description":"Eligible for the project board (including its archive view), independent of caller permissions. False for deleted records, orders, and drafts linked to an unconverted request."},"archivedAt":{"type":["string","null"],"format":"date-time","description":"When the project or order was archived, or null if it is not archived. A stored response or delivery from before this field existed does not have it."},"customerId":{"type":"string"},"propertyId":{"type":"string","description":"Linked property (Objekt), see `GET /api/v1/properties/{propertyId}`."},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["draft","planning","in_progress","on_hold","completed","cancelled"]},"statusId":{"type":"string","description":"ID of an organization-specific status column."},"type":{"type":"string","enum":["project","order"]},"projectNumber":{"type":"string"},"externalId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"estimatedBudget":{"type":"integer","description":"Amount in cents. Omitted without financials permission."},"actualCost":{"type":"integer","description":"Amount in cents. Omitted without financials permission."},"currency":{"type":"string","enum":["EUR"]},"address":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"geofenceRadiusMeters":{"type":"number"},"notes":{"type":"string"},"customFields":{"$ref":"#/components/schemas/CustomFieldValues"}}},"ProjectInput":{"type":"object","additionalProperties":true,"properties":{"customerId":{"type":"string"},"propertyId":{"type":["string","null"],"description":"Links the project to an active property of the organization. An\nunknown id answers 400, a deleted property 410 `property_deleted`.\nOn update, null removes the property and the tenant party of the\nproject; on create, null is the same as leaving it out.\n"},"name":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["draft","planning","in_progress","on_hold","completed","cancelled"]},"statusId":{"type":"string"},"type":{"type":"string","enum":["project","order"]},"projectNumber":{"type":"string"},"externalId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"estimatedBudget":{"type":"integer","description":"Amount in cents. Requires financials permission."},"actualCost":{"type":"integer","description":"Amount in cents. Requires financials permission."},"address":{"type":"string"},"city":{"type":"string"},"postalCode":{"type":"string"},"locationLatitude":{"type":"number"},"locationLongitude":{"type":"number"},"geofenceRadiusMeters":{"type":"number"},"notes":{"type":"string"},"customFields":{"$ref":"#/components/schemas/CustomFieldValues"}}},"CreateProjectInput":{"allOf":[{"$ref":"#/components/schemas/ProjectInput"},{"type":"object","required":["name"]}]},"ProjectResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Project"}}},"ProjectListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"ProjectMember":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","projectId","userId","role"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"projectId":{"type":"string"},"userId":{"type":"string"},"role":{"type":"string","enum":["owner","member","lead"]}}},"ProjectMemberInput":{"type":"object","additionalProperties":true,"required":["userId"],"properties":{"userId":{"type":"string"},"role":{"type":"string","enum":["owner","member","lead"],"default":"member"}}},"ProjectMemberResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ProjectMember"}}},"MaintenanceDevice":{"type":"object","additionalProperties":false,"properties":{"deviceType":{"type":"string"},"manufacturer":{"type":"string"},"model":{"type":"string"},"serialNumber":{"type":"string"},"yearBuilt":{"type":"integer"},"locationNote":{"type":"string"}}},"MaintenanceAsset":{"type":"object","additionalProperties":false,"required":["id","devices"],"description":"The equipment of the contract. Each contract has exactly one.","properties":{"id":{"type":"string"},"locationNote":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"devices":{"type":"array","description":"Devices and components of the equipment, in their order. Can be empty.","items":{"$ref":"#/components/schemas/MaintenanceDevice"}}}},"MaintenanceContract":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","contractNumber","status","customerId","intervalMonths","anchorMode"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"contractNumber":{"type":"string"},"title":{"type":"string"},"status":{"type":"string","enum":["aktiv","pausiert","beendet"],"description":"`beendet` is a cancelled contract; its data stays."},"customerId":{"type":"string"},"customerName":{"type":"string"},"propertyId":{"type":"string","description":"Linked property (Objekt). Missing when none is linked or the property was deleted."},"propertyName":{"type":"string"},"templateId":{"type":"string","description":"The maintenance type of the contract, from `GET /api/v1/maintenance-templates`. A deleted type stays on the contract."},"templateName":{"type":"string","description":"Name of the maintenance type. Holds no prices, so every reader gets it."},"intervalMonths":{"type":"integer"},"preferredMonth":{"type":"integer","minimum":1,"maximum":12},"anchorMode":{"type":"string","enum":["fixed_schedule","after_completion"],"description":"`fixed_schedule` keeps the rhythm of the calendar, `after_completion` counts the interval from the last completed visit."},"estimatedDurationMinutes":{"type":"integer"},"termStart":{"type":"string","format":"date-time"},"initialTermMonths":{"type":"integer"},"renewalMonths":{"type":"integer"},"noticePeriodMonths":{"type":"integer"},"notes":{"type":"string"},"nextDueDate":{"type":"string","format":"date","description":"Target month of the next open or scheduled visit, as the first day of the month. Only for active contracts that are not archived."},"nextVisitId":{"type":"string","description":"The visit behind `nextDueDate`, for `GET /api/v1/maintenance-visits/{visitId}`."},"archivedAt":{"type":"string","format":"date-time","description":"Set when the contract is archived. Archived contracts are listed only with `includeArchived=true`."},"address":{"type":"string","description":"Where the maintenance takes place, as one line. Address of the equipment, else of the customer."},"asset":{"$ref":"#/components/schemas/MaintenanceAsset"},"priceCents":{"type":"integer","description":"Flat price per visit in cents. Only with maintenance permission `full`."},"laborRateCents":{"type":"integer","description":"Hourly rate for extra hours in cents. Only with maintenance permission `full`."},"billingMode":{"type":"string","enum":["advance","after_visit"],"description":"Only with maintenance permission `full`."},"catalogPositions":{"type":"array","description":"Frozen service positions of the contract. Only with maintenance permission `full`.","items":{"type":"object","additionalProperties":false,"required":["title","quantity","unitPriceCents"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"unitPriceCents":{"type":"integer"}}}}}},"MaintenanceContractResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MaintenanceContract"}}},"MaintenanceContractPositionInput":{"type":"object","additionalProperties":false,"required":["title","quantity","unitPriceCents"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","maxLength":40},"unitPriceCents":{"type":"integer","minimum":0}}},"MaintenanceContractDeviceInput":{"type":"object","additionalProperties":false,"required":["deviceType","manufacturer"],"properties":{"deviceType":{"type":"string","minLength":1,"maxLength":120},"manufacturer":{"type":"string","minLength":1,"maxLength":120},"model":{"type":"string","maxLength":120},"serialNumber":{"type":"string","maxLength":120},"yearBuilt":{"type":"integer","minimum":1900,"maximum":2100},"locationNote":{"type":"string","maxLength":200}}},"CreateMaintenanceContractInput":{"type":"object","additionalProperties":false,"required":["customerId","title"],"description":"The same required fields as the contract dialog of craftivo. With `templateId`, `intervalMonths`, `preferredMonth`, `priceCents`, `notes` and `catalogPositions` come from the template unless you send them, and the forms of the template are assigned. Without it, `intervalMonths`, `preferredMonth` and `priceCents` are required.","properties":{"customerId":{"type":"string"},"title":{"type":"string","minLength":1,"maxLength":200,"description":"The name of the contract in the maintenance overview."},"propertyId":{"type":"string"},"templateId":{"type":"string","description":"The maintenance type, from `GET /api/v1/maintenance-templates`."},"status":{"type":"string","enum":["aktiv","pausiert","beendet"],"default":"aktiv"},"intervalMonths":{"type":"integer","minimum":1},"preferredMonth":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"`null` is the deliberate choice of no preferred month."},"anchorMode":{"type":"string","enum":["fixed_schedule","after_completion"],"default":"fixed_schedule"},"priceCents":{"type":"integer","minimum":0,"description":"Flat price per visit in cents."},"laborRateCents":{"type":"integer","minimum":0},"billingMode":{"type":"string","enum":["advance","after_visit"],"default":"after_visit"},"estimatedDurationMinutes":{"type":"integer","minimum":1},"termStart":{"type":"string","format":"date-time"},"initialTermMonths":{"type":"integer","minimum":0},"renewalMonths":{"type":"integer","minimum":0},"noticePeriodMonths":{"type":"integer","minimum":0},"catalogPositions":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/MaintenanceContractPositionInput"}},"reminderOptIn":{"type":"boolean","default":false,"description":"Customer reminders for this contract."},"reminderLeadDays":{"type":"integer","minimum":0},"notes":{"type":["string","null"],"maxLength":2000,"description":"`null` or an empty text means no notes, also against the template."},"asset":{"type":"object","additionalProperties":false,"required":["street","postalCode","city"],"description":"Where the maintenance takes place. Without it the customer's address is copied; a customer without a complete address answers 400.","properties":{"street":{"type":"string","maxLength":200},"houseNumber":{"type":"string","maxLength":40},"postalCode":{"type":"string","maxLength":20},"city":{"type":"string","maxLength":120},"country":{"type":"string","maxLength":120}}},"devices":{"type":"array","maxItems":50,"description":"Leave out for work without equipment, such as cleaning a gutter.","items":{"$ref":"#/components/schemas/MaintenanceContractDeviceInput"}},"firstVisitCompletedAt":{"type":"string","format":"date-time","description":"The first visit was done before the contract existed. It is stored as completed and the next open visit counts from it. Must not be in the future."}}},"UpdateMaintenanceContractInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Send only what changes. `null` clears an optional field. `title`, `priceCents`, `intervalMonths` and the required address fields cannot be cleared, as in the contract dialog. The status has its own route.","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"propertyId":{"type":["string","null"]},"templateId":{"type":["string","null"],"description":"Changes or removes the maintenance type. Values of the contract stay, as in craftivo; a type deleted in the meantime may stay on the contract."},"intervalMonths":{"type":"integer","minimum":1},"preferredMonth":{"type":["integer","null"],"minimum":1,"maximum":12,"description":"Like `intervalMonths` and `anchorMode` it moves an open visit of an active contract to the new due month. A scheduled visit stays."},"anchorMode":{"type":"string","enum":["fixed_schedule","after_completion"]},"priceCents":{"type":"integer","minimum":0},"laborRateCents":{"type":["integer","null"],"minimum":0},"billingMode":{"type":"string","enum":["advance","after_visit"]},"estimatedDurationMinutes":{"type":["integer","null"],"minimum":1},"termStart":{"type":["string","null"],"format":"date-time"},"initialTermMonths":{"type":["integer","null"],"minimum":0},"renewalMonths":{"type":["integer","null"],"minimum":0},"noticePeriodMonths":{"type":["integer","null"],"minimum":0},"catalogPositions":{"type":"array","maxItems":200,"description":"Replaces the frozen positions. An empty list removes them.","items":{"$ref":"#/components/schemas/MaintenanceContractPositionInput"}},"reminderOptIn":{"type":"boolean"},"reminderLeadDays":{"type":["integer","null"],"minimum":0},"notes":{"type":["string","null"],"maxLength":2000},"asset":{"type":"object","additionalProperties":false,"description":"Changes single address fields. `houseNumber` and `country` take `null`.","properties":{"street":{"type":"string","minLength":1,"maxLength":200},"houseNumber":{"type":["string","null"],"maxLength":40},"postalCode":{"type":"string","minLength":1,"maxLength":20},"city":{"type":"string","minLength":1,"maxLength":120},"country":{"type":["string","null"],"maxLength":120}}},"devices":{"type":"array","maxItems":50,"description":"Replaces the devices. An empty list removes them. Photos stay with the device at the same position in the list; the last ones leave with it.","items":{"$ref":"#/components/schemas/MaintenanceContractDeviceInput"}}}},"SetMaintenanceContractStatusInput":{"type":"object","additionalProperties":false,"required":["status"],"properties":{"status":{"type":"string","enum":["aktiv","pausiert","beendet"]}}},"MaintenanceTemplate":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","intervalMonths","catalogPositions"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"intervalMonths":{"type":"integer"},"preferredMonth":{"type":"integer","minimum":1,"maximum":12},"priceCents":{"type":"integer"},"notes":{"type":"string"},"catalogPositions":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["title","quantity","unitPriceCents"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"quantity":{"type":"number"},"unit":{"type":"string"},"unitPriceCents":{"type":"integer"}}}}}},"MaintenanceTemplateListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceTemplate"}}}},"MaintenanceContractWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["maintenance_contract.created","maintenance_contract.updated","maintenance_contract.archived","maintenance_contract.restored"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["maintenanceContract"],"properties":{"maintenanceContract":{"oneOf":[{"$ref":"#/components/schemas/MaintenanceContract"},{"type":"object","additionalProperties":false,"description":"For an endpoint whose creator lacks the maintenance permission.","required":["id"],"properties":{"id":{"type":"string"}}}]}}}}},"MaintenanceContractListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceContract"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"MaintenanceVisit":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","contractId","contractNumber","customerId","status","dueDate","memberIds"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"contractId":{"type":"string"},"contractNumber":{"type":"string"},"contractTitle":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"propertyId":{"type":"string"},"status":{"type":"string","enum":["offen","eingeplant","erledigt"],"description":"`offen` is due but not on the planning board, `eingeplant` has an assignment, `erledigt` is done."},"dueDate":{"type":"string","format":"date","description":"Target month as its first day. The real day is `scheduledDate`."},"completedAt":{"type":"string","format":"date-time"},"completedByMemberId":{"type":"string","description":"Member as in `GET /api/v1/members`."},"customerConfirmedAt":{"type":"string","format":"date-time","description":"When the customer confirmed the date through the reminder link."},"scheduleEntryId":{"type":"string","description":"Assignment on the planning board, as in `GET /api/v1/schedule-entries/{entryId}`."},"scheduledDate":{"type":"string","format":"date"},"startTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin). Missing for all-day assignments."},"endTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin)."},"memberIds":{"type":"array","description":"Members of the assignment: directly assigned plus the current members of the crew. Only members that `GET /api/v1/members` lists. Can be empty.","items":{"type":"string"}},"address":{"type":"string","description":"Where the maintenance takes place, as one line. Address of the equipment, else of the customer."},"archivedAt":{"type":"string","format":"date-time","description":"Set on open visits of an archived contract. Archived visits are never listed."}}},"MaintenanceVisitDetail":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","contractId","contractNumber","customerId","status","dueDate","memberIds","protocolFormInstanceIds","extras"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"contractId":{"type":"string"},"contractNumber":{"type":"string"},"contractTitle":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"propertyId":{"type":"string"},"status":{"type":"string","enum":["offen","eingeplant","erledigt"],"description":"`offen` is due but not on the planning board, `eingeplant` has an assignment, `erledigt` is done."},"dueDate":{"type":"string","format":"date","description":"Target month as its first day. The real day is `scheduledDate`."},"completedAt":{"type":"string","format":"date-time"},"completedByMemberId":{"type":"string","description":"Member as in `GET /api/v1/members`."},"customerConfirmedAt":{"type":"string","format":"date-time","description":"When the customer confirmed the date through the reminder link."},"scheduleEntryId":{"type":"string","description":"Assignment on the planning board, as in `GET /api/v1/schedule-entries/{entryId}`."},"scheduledDate":{"type":"string","format":"date"},"startTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin). Missing for all-day assignments."},"endTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin)."},"memberIds":{"type":"array","description":"Members of the assignment: directly assigned plus the current members of the crew. Only members that `GET /api/v1/members` lists. Can be empty.","items":{"type":"string"}},"address":{"type":"string","description":"Where the maintenance takes place, as one line. Address of the equipment, else of the customer."},"archivedAt":{"type":"string","format":"date-time","description":"Set on open visits of an archived contract. Archived visits are never listed."},"technicianNote":{"type":"string","description":"Note of the technician on the visit."},"technicianNoteRelevance":{"type":"string","enum":["office_now","next_visit"]},"carriedForwardNote":{"type":"string","description":"Note carried over from the previous visit."},"protocolFormInstanceIds":{"type":"array","description":"Form instances of the maintenance protocols of the visit, oldest first, as in `GET /api/v1/form-instances`. Can be empty.","items":{"type":"string"}},"extras":{"type":"array","description":"Extra material and hours recorded on the visit, without prices.","items":{"type":"object","additionalProperties":false,"required":["kind","title","quantity"],"properties":{"kind":{"type":"string","enum":["material","labor"]},"title":{"type":"string","description":"Name of the material. For hours an optional note, can be empty."},"quantity":{"type":"number","description":"Amount of material, or hours for `labor`."},"unit":{"type":"string"}}}},"invoiceDraftId":{"type":"string","description":"Invoice draft created for this visit, as in `GET /api/v1/invoices/{invoiceId}`. Only with maintenance permission `full`, and missing once the draft is deleted."},"extrasInvoiceDraftId":{"type":"string","description":"Invoice draft for the extras of a visit billed in advance. Only with maintenance permission `full`."}}},"MaintenanceVisitResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MaintenanceVisitDetail"}}},"CompleteMaintenanceVisitInput":{"type":"object","additionalProperties":false,"description":"All fields are optional; an empty body completes the visit now.","properties":{"completedAt":{"type":"string","format":"date-time","description":"When the work was done. Defaults to now; must not be in the future."},"technicianNote":{"type":"string","minLength":1,"maxLength":2000,"description":"The result of the visit. Needs `technicianNoteRelevance`. A visit holds one note: another existing note answers 409 `technician_note_exists`."},"technicianNoteRelevance":{"type":"string","enum":["office_now","next_visit"],"description":"`office_now` shows the note to the office as open. `next_visit` carries it over to the next visit."}}},"MaintenanceVisitCompleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["visit","nextVisit","alreadyCompleted"],"properties":{"visit":{"$ref":"#/components/schemas/MaintenanceVisitDetail"},"nextVisit":{"description":"The next open visit of an active contract; `null` when none was created.","oneOf":[{"$ref":"#/components/schemas/MaintenanceVisitDetail"},{"type":"null"}]},"alreadyCompleted":{"type":"boolean","description":"The visit was completed before. Nothing changed, also not the note."}}}}},"ScheduleMaintenanceVisitsInput":{"type":"object","additionalProperties":false,"required":["date"],"description":"Without `memberIds` and `teamId` the visit gets its date and stays open for a team to pick up, like in the web app.","properties":{"date":{"type":"string","format":"date","description":"The day of the visit, not the due month."},"startTime":{"type":"string","description":"`HH:mm`. Only together with `endTime`; without both the visit takes the whole day.","examples":["08:00"]},"endTime":{"type":"string","examples":["10:30"]},"memberIds":{"type":"array","description":"Ids from `GET /api/v1/members`.","items":{"type":"string"}},"teamId":{"type":"string"},"force":{"type":"boolean","default":false,"description":"Schedule despite a double booking or an absence, like \"Trotzdem einplanen\" in craftivo. The conflicts return in `meta.conflicts`."}}},"MaintenanceVisitScheduleResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["visit"],"properties":{"visit":{"$ref":"#/components/schemas/MaintenanceVisitDetail"}}},"meta":{"type":"object","additionalProperties":false,"properties":{"conflicts":{"type":"array","description":"Conflicts accepted with `force`.","items":{"$ref":"#/components/schemas/ScheduleConflict"}}}}}},"MaintenanceVisitWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["maintenance_visit.scheduled","maintenance_visit.completed"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["maintenanceVisit"],"properties":{"maintenanceVisit":{"oneOf":[{"$ref":"#/components/schemas/MaintenanceVisit"},{"type":"object","additionalProperties":false,"description":"For an endpoint whose creator lacks the maintenance permission.","required":["id"],"properties":{"id":{"type":"string"}}}]}}}}},"MaintenanceVisitListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MaintenanceVisit"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Member":{"type":"object","additionalProperties":false,"description":"Joining is the only tracked write on a membership, so `createdAt` and `updatedAt` both mirror the membership's `joinedAt` (CI-1700). `role` and `memberType` use the same vocabulary as `/me` (CI-3051).","required":["id","createdAt","updatedAt","name","role","memberType"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"role":{"type":"string","enum":["owner","office","site_manager","technician"]},"memberType":{"type":"string","description":"Effective workplace of the member. `field` members use the mobile app on site, `office` members work on the desktop app.","enum":["field","office"]}}},"MemberListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}},"Position":{"type":"object","additionalProperties":false,"description":"An entry of the position library. Responses never contain purchase prices or calculations; `costPriceCents` can only be written. Optional fields are omitted when empty.","required":["id","title","unit","quantity","itemType","isPriceQuantityDependent"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"category":{"type":"string"},"unit":{"type":"string"},"unitPriceCents":{"type":"integer","description":"Net sales price per unit in cents, as stored in the library. Omitted when the member who created the API key may not see sales prices."},"quantity":{"type":"number","description":"Default quantity."},"articleNumber":{"type":"string","description":"Not unique within an organization."},"itemType":{"type":"string","enum":["position","material"]},"isPriceQuantityDependent":{"type":"boolean","description":"`true` when a price calculation sets the unit price depending on the quantity. Do not compute `unitPriceCents` times a quantity in that case; `unitPriceCents` is not the price for another quantity."}}},"PositionResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Position"}}},"PositionListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Position"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CreatePositionInput":{"type":"object","additionalProperties":false,"required":["title","unit","quantity","unitPriceCents"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"category":{"type":"string","maxLength":80},"unit":{"type":"string","minLength":1,"maxLength":20},"quantity":{"type":"number","exclusiveMinimum":0,"maximum":999999,"description":"Default quantity."},"unitPriceCents":{"type":"integer","minimum":0,"maximum":99999999,"description":"Net sales price per unit in cents. Needs the permission to see sales prices."},"articleNumber":{"type":"string","maxLength":120,"description":"Not unique within an organization."},"itemType":{"type":"string","enum":["position","material"],"default":"position"},"costPriceCents":{"type":"number","minimum":0,"maximum":99999999,"description":"Purchase price per unit in cents with up to three decimals, for example `123.456`. Stored as sent, more decimals answer 400. Needs the `financials` permission. Write-only: no response contains it."}}},"UpdatePositionInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Send only what changes.","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"description":{"type":["string","null"],"maxLength":2000,"description":"`null` or an empty text removes it."},"category":{"type":["string","null"],"maxLength":80,"description":"`null` or an empty text removes it."},"unit":{"type":"string","minLength":1,"maxLength":20},"quantity":{"type":"number","exclusiveMinimum":0,"maximum":999999,"description":"Default quantity. An entry with a price calculation answers 409 `position_price_calculated`."},"unitPriceCents":{"type":"integer","minimum":0,"maximum":99999999,"description":"Net sales price per unit in cents. Needs the permission to see sales prices. An entry with a price calculation answers 409 `position_price_calculated`."},"articleNumber":{"type":["string","null"],"maxLength":120,"description":"`null` or an empty text removes it."},"itemType":{"type":"string","enum":["position","material"]},"costPriceCents":{"type":["number","null"],"minimum":0,"maximum":99999999,"description":"Purchase price per unit in cents with up to three decimals; `null` removes it. Stored as sent, more decimals answer 400. Needs the `financials` permission. Write-only: no response contains it. An entry from a wholesaler connection answers 409 `position_ids_price`."}}},"Absence":{"type":"object","additionalProperties":false,"required":["id","memberId","type","status","startDate","endDate","isHalfDay","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"memberId":{"type":"string","description":"Member id as returned by `GET /api/v1/members`."},"type":{"type":"string","enum":["vacation","sick","personal","unpaid","parental_leave","other","absent"],"description":"`personal` covers personal leave and training, `unpaid` an unpaid day off. `absent` replaces `sick` for API users without the employees permission at level `full`."},"status":{"type":"string","enum":["pending","approved","rejected"]},"startDate":{"type":"string","format":"date","description":"First day (inclusive), `YYYY-MM-DD`."},"endDate":{"type":"string","format":"date","description":"Last day (inclusive), `YYYY-MM-DD`."},"isHalfDay":{"type":"boolean"},"startTime":{"type":"string","description":"Start of a short absence on a single day, `HH:MM` local time."},"endTime":{"type":"string","description":"End of a short absence on a single day, `HH:MM` local time."},"companyVacationId":{"type":"string","description":"Set when the absence belongs to a company vacation from `GET /api/v1/company-vacations`."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"AbsenceListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Absence"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"AbsenceResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Absence"}}},"AbsenceDeleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"CreateAbsenceInput":{"type":"object","additionalProperties":false,"required":["type","startDate","endDate"],"properties":{"memberId":{"type":"string","description":"Member id as returned by `GET /api/v1/members`. Default is the key's creator."},"type":{"type":"string","enum":["vacation","sick","personal","unpaid","parental_leave","other"],"description":"`absent` only hides sick leave in responses and answers 400 here."},"startDate":{"type":"string","format":"date","description":"First day (inclusive), `YYYY-MM-DD`."},"endDate":{"type":"string","format":"date","description":"Last day (inclusive), `YYYY-MM-DD`."},"notes":{"type":"string","maxLength":2000,"description":"Internal note. No response contains it."},"isHalfDay":{"type":"boolean"},"startTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Send together with `endTime`, `HH:MM` local time."},"endTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Send together with `startTime`, `HH:MM` local time."},"allowOverlap":{"type":"boolean","description":"Save even if another absence of the member overlaps."}}},"UpdateAbsenceInput":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"type":{"type":"string","enum":["vacation","sick","personal","unpaid","parental_leave","other"],"description":"`absent` only hides sick leave in responses and answers 400 here."},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"notes":{"type":"string","maxLength":2000,"description":"Internal note. No response contains it. An empty text removes it."},"isHalfDay":{"type":"boolean"},"startTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Send together with `endTime`, `HH:MM` local time."},"endTime":{"type":"string","pattern":"^([01][0-9]|2[0-3]):[0-5][0-9]$","description":"Send together with `startTime`, `HH:MM` local time."}}},"RejectAbsenceInput":{"type":"object","additionalProperties":false,"properties":{"rejectionReason":{"type":"string","maxLength":2000,"description":"Goes to the member by email. No API response contains it."}}},"CompanyVacation":{"type":"object","additionalProperties":false,"required":["id","name","startDate","endDate","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"CompanyVacationListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CompanyVacation"}}}},"Holiday":{"type":"object","additionalProperties":false,"required":["id","name","date","isRecurring","source"],"properties":{"id":{"type":"string","description":"Id of the holiday. A recurring holiday returns the same id for every year."},"name":{"type":"string"},"date":{"type":"string","format":"date","description":"The date in the requested range, `YYYY-MM-DD`."},"isRecurring":{"type":"boolean","description":"Recurs every year on the same day and month."},"source":{"type":"string","enum":["auto","manual"],"description":"`auto` was imported from the holiday region of the organization, `manual` was added by hand."}}},"SandboxReset":{"type":"object","additionalProperties":false,"required":["id","status","source","deletedDocuments","startedAt","finishedAt"],"properties":{"id":{"type":["string","null"],"description":"Id of the latest reset run; `null` before the first reset."},"status":{"type":"string","enum":["idle","running","completed","failed"],"description":"`idle` before the first reset, `running` while craftivo deletes and\nrecreates the data, `completed` when the sample data is back,\n`failed` when the run stopped (for example because the organization\nstopped being a practice account). After `failed`, start a new reset.\n"},"source":{"type":["string","null"],"enum":["api","settings",null],"description":"Where the run started; `settings` is the button in the craftivo settings."},"deletedDocuments":{"type":"integer","minimum":0,"description":"Records deleted so far, including dependent records."},"startedAt":{"type":["string","null"],"format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time"}}},"SandboxResetResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SandboxReset"}}},"HolidayListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Holiday"}}}},"ScheduleEntry":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","title","status","isPreplanning","isNote","startDate","endDate","memberIds"],"properties":{"id":{"type":"string","description":"Id of the assignment. For a date of a recurring assignment, the id of that date; `GET /api/v1/schedule-entries/{entryId}` takes the `recurrenceId` instead."},"recurrenceId":{"type":"string","description":"Only on dates of a recurring assignment: the id of the recurring assignment."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"title":{"type":"string"},"description":{"type":"string"},"notes":{"type":"string","description":"Instructions of the planner (\"Was ist zu tun?\")."},"teamNotes":{"type":"string","description":"Shared note of the assigned members."},"status":{"type":"string","enum":["draft","tentative","planned","confirmed","scheduled","in_progress","paused","completed","cancelled"],"description":"`draft` and `tentative` are not yet released to the app. A deleted date of a recurring assignment has status `cancelled`."},"entryType":{"type":"string","enum":["project","workBlock","maintenance"],"description":"`project` is a project bar in the upper part of the board, `workBlock` an assignment of members or a crew, `maintenance` a maintenance visit. Missing on older assignments."},"isPreplanning":{"type":"boolean","description":"`true` for the pre-planning bar of a project: entry type `project`, status `draft`, no crew and no members."},"isNote":{"type":"boolean","description":"A note on the board instead of an assignment; it books nobody."},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date","description":"Inclusive. Equal to `startDate` for a date of a recurring assignment."},"startTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin). Missing for all-day assignments."},"endTime":{"type":"string","description":"Local time `HH:MM` (Europe/Berlin)."},"dayTimeOverrides":{"type":"array","description":"Days of a multi-day assignment whose times differ from `startTime`/`endTime`.","items":{"type":"object","additionalProperties":false,"required":["date","startTime","endTime"],"properties":{"date":{"type":"string","format":"date"},"startTime":{"type":"string"},"endTime":{"type":"string"}}}},"recurrenceRule":{"type":"string","description":"RFC 5545 RRULE. Only on a recurring assignment itself (`GET /api/v1/schedule-entries/{entryId}`), not on its dates."},"projectId":{"type":"string"},"teamId":{"type":"string","description":"Crew as in `GET /api/v1/teams`."},"customerId":{"type":"string","description":"Customer of the project or of the maintenance contract."},"memberIds":{"type":"array","description":"Assigned members: directly assigned plus the current members of the crew. Only members that `GET /api/v1/members` lists. Can be empty.","items":{"type":"string"}},"address":{"type":"string","description":"Where the work takes place, as one line: the property of the project, else the project address, else the customer address. For maintenance visits the address of the equipment, else the customer."}}},"ScheduleEntryResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScheduleEntry"}}},"ScheduleEntryWriteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ScheduleEntry"},"meta":{"type":"object","additionalProperties":false,"description":"Only when the period has a holiday or `force` overrode a conflict.","required":["conflicts"],"properties":{"conflicts":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleConflict"}}}}}},"ScheduleEntryDeleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"ScheduleConflict":{"type":"object","additionalProperties":false,"description":"One conflict of the planning board. The kind of an absence and its reason stay on the server. Without the time tracking permission (`time_tracking:view`) `absences` is missing. An assignment of a project the key owner may not see stays listed as \"Interner Termin\", without `projectId`.","required":["type","severity"],"properties":{"type":{"type":"string","enum":["team_double_booking","member_double_booking","team_member_absent","public_holiday"]},"severity":{"type":"string","enum":["low","medium","high"]},"entries":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","title","status","startDate","endDate","memberIds"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"status":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"startTime":{"type":"string","examples":["07:30"]},"endTime":{"type":"string","examples":["16:00"]},"projectId":{"type":"string"},"teamId":{"type":"string"},"memberIds":{"type":"array","items":{"type":"string"}}}}},"absences":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["memberId","startDate","endDate"],"properties":{"memberId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"}}}},"holidays":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["date","name"],"properties":{"date":{"type":"string","format":"date"},"name":{"type":"string"}}}}}},"ScheduleEntryConflictError":{"type":"object","additionalProperties":false,"required":["error"],"properties":{"error":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","description":"`schedule_conflict` with `details.conflicts`, `schedule_duplicate`, `use_web_planner`, `invalid_status_transition` with `details.from` and `details.to`, or for a maintenance visit `visit_already_scheduled` with `details.visitId` and `details.scheduleEntryId`, `contract_not_schedulable` or `no_open_visit`.","examples":["schedule_conflict"]},"message":{"type":"string"},"details":{"type":"object","additionalProperties":true,"properties":{"conflicts":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleConflict"}}}}}}}},"CreateScheduleEntryInput":{"type":"object","additionalProperties":false,"required":["projectId","startDate"],"description":"Needs `memberIds` or `teamId`. The title is the project name. All members of `teamId` are planned as well.","properties":{"projectId":{"type":"string"},"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date","description":"Last day. Defaults to `startDate`; a later date makes it a multi-day assignment."},"startTime":{"type":"string","description":"`HH:mm`. Only together with `endTime`; without both the assignment takes the whole day.","examples":["07:30"]},"endTime":{"type":"string","examples":["16:00"]},"memberIds":{"type":"array","description":"Ids from `GET /api/v1/members`.","items":{"type":"string"}},"teamId":{"type":"string"},"notes":{"type":"string"},"status":{"type":"string","enum":["planned","confirmed"],"default":"planned"},"force":{"type":"boolean","default":false,"description":"Plan despite a double booking or an absence, like \"Trotzdem einplanen\" in the planning board. A double booking is then marked as planned in parallel and does not block later changes."}}},"UpdateScheduleEntryInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Send only what changes. Moving a date or changing the people checks for conflicts; a double booking also blocks other field changes, like in the planning board. Changing only the status blocks only when a draft or tentative assignment becomes `planned` or `confirmed`.","properties":{"startDate":{"type":"string","format":"date"},"endDate":{"type":"string","format":"date"},"startTime":{"type":["string","null"],"description":"`HH:mm`. Only together with `endTime`; both `null` make it an all-day assignment."},"endTime":{"type":["string","null"]},"memberIds":{"type":"array","description":"Replaces the direct members. Members of the team stay. At least one member or a team has to remain.","items":{"type":"string"}},"teamId":{"type":["string","null"]},"notes":{"type":["string","null"]},"status":{"type":"string","enum":["planned","confirmed","cancelled"],"description":"Only forward, like in the planning board: `planned` to `confirmed`, anything open to `cancelled`. Others answer 409 `invalid_status_transition`."},"force":{"type":"boolean","default":false}}},"ScheduleEntryWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["schedule_entry.created","schedule_entry.updated","schedule_entry.deleted"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["scheduleEntry"],"properties":{"scheduleEntry":{"oneOf":[{"$ref":"#/components/schemas/ScheduleEntry"},{"type":"object","additionalProperties":false,"description":"For an endpoint whose creator may not see the assignment.","required":["id"],"properties":{"id":{"type":"string"}}}]}}}}},"ScheduleEntryListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ScheduleEntry"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Task":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","type","title","status","dueAt","assigneeId","dueReminder","createdVia"],"properties":{"attachments":{"type":"array","description":"Only in `GET /api/v1/tasks/{taskId}`.","items":{"$ref":"#/components/schemas/TaskAttachment"}},"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"type":{"type":"string","enum":["task","call","appointment","measurement","email","reminder"]},"title":{"type":"string"},"description":{"type":"string"},"status":{"type":"string","enum":["open","completed"]},"completedAt":{"type":"string","format":"date-time"},"dueAt":{"type":"string","format":"date-time"},"dueEndAt":{"type":"string","format":"date-time"},"dueDayTimes":{"type":"array","description":"Per-day times of a multi-day task (CI-4590): one row per calendar day (Europe/Berlin) from dueAt to dueEndAt. Only present when at least one day differs; otherwise the task spans dueAt–dueEndAt as before.","items":{"$ref":"#/components/schemas/TaskDayTime"}},"priority":{"type":"string","enum":["urgent_today","low","medium","high"]},"assigneeId":{"type":"string"},"participantIds":{"type":"array","items":{"type":"string"}},"projectId":{"type":"string"},"customerId":{"type":"string"},"customerRequestId":{"type":"string","description":"Customer request the task belongs to."},"location":{"type":"string","description":"Location of an appointment or address of a measurement. Other task types have none."},"dueReminder":{"type":"boolean","description":"false = the task sends no due notification (bell or push) to anyone."},"createdVia":{"type":"string","enum":["app","api"]}}},"TaskDayTime":{"type":"object","additionalProperties":false,"required":["date","startTime","endTime"],"properties":{"date":{"type":"string","format":"date","description":"Calendar day in Europe/Berlin (YYYY-MM-DD)."},"startTime":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","example":"08:00"},"endTime":{"type":"string","pattern":"^(?:[01][0-9]|2[0-3]):[0-5][0-9]$","example":"09:00"}}},"CreateTaskInput":{"type":"object","additionalProperties":false,"required":["title"],"properties":{"title":{"type":"string","minLength":1},"description":{"type":"string"},"type":{"type":"string","enum":["task","call","appointment","measurement","email","reminder"],"default":"task"},"dueAt":{"type":"string","format":"date-time","description":"Defaults to the end of today in Europe/Berlin."},"dueEndAt":{"type":"string","format":"date-time"},"dueDayTimes":{"type":"array","description":"Per-day times for a multi-day task: one row per calendar day (Europe/Berlin) from dueAt to dueEndAt, at least two rows, each at least 15 minutes. The first row must start at dueAt, the last must end at dueEndAt. Not allowed on recurring tasks.","items":{"$ref":"#/components/schemas/TaskDayTime"}},"priority":{"type":"string","enum":["urgent_today","low","medium","high"]},"dueReminder":{"type":"boolean","description":"false = no due notification for this task. Omitted = unchanged (default true on create)."},"assigneeId":{"type":"string","description":"Defaults to the assignee configured on the write API key."},"participantIds":{"type":"array","items":{"type":"string"}},"projectId":{"type":"string"},"projectExternalId":{"type":"string"},"customerId":{"type":"string"},"customerExternalId":{"type":"string"},"customerRequestId":{"type":"string","description":"Attaches the task to a customer request; it then shows in the request\nin craftivo. The customer must be the request's customer, a project\nthe project linked to the request; otherwise 400. An unknown request\nreturns 404. Cannot be changed later.\n"},"location":{"type":"string","minLength":1,"description":"Only for `appointment` and `measurement`; other types return 400.\nOmitted on an appointment or measurement with `customerRequestId`:\nthe site of the request (its property, otherwise its address) is\nused, never the customer's address.\n"}}},"UpdateTaskInput":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"title":{"type":"string","minLength":1},"description":{"type":["string","null"]},"type":{"type":"string","enum":["task","call","appointment","measurement","email","reminder"]},"status":{"type":"string","enum":["open","completed"]},"dueAt":{"type":"string","format":"date-time"},"dueEndAt":{"type":["string","null"],"format":"date-time"},"dueDayTimes":{"type":["array","null"],"description":"Per-day times (see CreateTaskInput). null removes them. When dueAt or dueEndAt change without dueDayTimes, existing rows are kept only if they still match; otherwise they are dropped.","items":{"$ref":"#/components/schemas/TaskDayTime"}},"priority":{"type":["string","null"],"enum":["urgent_today","low","medium","high",null]},"dueReminder":{"type":"boolean","description":"false = no due notification for this task. Omitted = unchanged (default true on create)."},"assigneeId":{"type":"string"},"participantIds":{"type":"array","items":{"type":"string"}},"projectId":{"type":["string","null"]},"projectExternalId":{"type":"string"},"customerId":{"type":["string","null"]},"customerExternalId":{"type":"string"},"location":{"type":["string","null"],"minLength":1,"description":"Only for `appointment` and `measurement`; other types return 400. Null clears it."}}},"TaskAttachment":{"type":"object","additionalProperties":false,"required":["id","taskId","createdAt","fileName","mimeType","kind","fileSize","url"],"properties":{"id":{"type":"string"},"taskId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"fileName":{"type":"string"},"mimeType":{"type":"string"},"kind":{"type":"string","enum":["image","video","document"]},"fileSize":{"type":"integer","description":"Size in bytes."},"url":{"type":["string","null"],"description":"Download link. `null` when the stored file is no longer available."}}},"TaskAttachmentResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/TaskAttachment"}}},"AttachTaskFileInput":{"type":"object","additionalProperties":false,"required":["fileId"],"properties":{"fileId":{"type":"string","minLength":1,"description":"File id from `POST /api/v1/files`."}}},"TaskComment":{"type":"object","additionalProperties":false,"required":["id","taskId","createdAt","body","authorId","authorName","createdVia"],"properties":{"id":{"type":"string"},"taskId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"body":{"type":"string"},"authorId":{"type":"string","description":"Member id from `GET /api/v1/members`. With `createdVia: api` this is the creator of the API key."},"authorName":{"type":"string","description":"Name at the time of writing. With `createdVia: api` this is the name of the API key."},"createdVia":{"type":"string","enum":["app","api"]}}},"TaskCommentResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/TaskComment"}}},"TaskCommentListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TaskComment"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CreateTaskCommentInput":{"type":"object","additionalProperties":false,"required":["body"],"properties":{"body":{"type":"string","minLength":1,"maxLength":10000}}},"TaskDeletedResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"TaskResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Task"}}},"TaskListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Task"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"Team":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","isActive","memberIds"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"color":{"type":"string","description":"Hex color as shown in the app. Missing when the crew has no color."},"isActive":{"type":"boolean","description":"`false` for a deactivated crew. The list only returns active crews; `GET /api/v1/teams/{teamId}` also returns deactivated ones."},"memberIds":{"type":"array","description":"Member ids in order of joining the crew, including the leader. Only members that `GET /api/v1/members` lists. Can be empty.","items":{"type":"string"}},"leaderId":{"type":"string","description":"Crew leader: the member with the leader role who joined first. Missing when no listed member has that role. Further leaders stay in `memberIds`."}}},"TeamResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Team"}}},"TeamListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Team"}}}},"TimeEntryCategory":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","color","isBillable","isDefault"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"color":{"type":"string","description":"Hex color as shown in the app."},"isBillable":{"type":"boolean"},"isDefault":{"type":"boolean"}}},"TimeEntryCategoryListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TimeEntryCategory"}}}},"TimeEntry":{"type":"object","additionalProperties":false,"description":"Public view of a time entry. GPS snapshots, approval details, and billing locks are internal and never exposed (CI-3052).","required":["id","createdAt","updatedAt","userId","date","startTime","endTime","breakMinutes","durationMinutes","status"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"userId":{"type":"string","description":"Member id as returned by `GET /api/v1/members`."},"projectId":{"type":"string"},"categoryId":{"type":"string"},"date":{"type":"string","format":"date","description":"Entry date in the organization's time zone, `YYYY-MM-DD`."},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"breakMinutes":{"type":"integer","minimum":0},"durationMinutes":{"type":"integer","minimum":0,"description":"Worked minutes between `startTime` and `endTime` minus `breakMinutes`."},"description":{"type":"string"},"status":{"type":"string","enum":["draft","submitted","approved","rejected"]},"startTimeMark":{"description":"Set when the start time comes from the phone and differs from the server time, like the column \"Beginn erfasst\" of the time export. `startTime` stays the phone time. `null` when the start time is close to the server time, was entered by hand, or predates this mark.","oneOf":[{"$ref":"#/components/schemas/TimeEntryClockMark"},{"type":"null"}]},"endTimeMark":{"description":"Same as `startTimeMark` for the end time (column \"Ende erfasst\" of the time export). `endTime` stays the phone time.","oneOf":[{"$ref":"#/components/schemas/TimeEntryClockMark"},{"type":"null"}]}}},"TimeEntryClockMark":{"type":"object","additionalProperties":false,"description":"Why a clock time of a time entry differs from the server time.","required":["kind","serverTime"],"properties":{"kind":{"type":"string","enum":["offline","deviceSkew"],"description":"`offline`: the phone had no network and sent the clock time later. `deviceSkew`: sent live, but the phone clock was more than 5 minutes off the server time."},"serverTime":{"type":"string","format":"date-time","description":"When the clock time arrived at the server."}}},"TimeEntryResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/TimeEntry"}}},"TimeEntryListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TimeEntry"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CreateTimeEntryInput":{"type":"object","additionalProperties":false,"required":["userId","startTime","endTime"],"description":"Exactly one of `projectId` or `orderId` is required.","properties":{"userId":{"type":"string","description":"Member id as returned by `GET /api/v1/members`."},"projectId":{"type":"string"},"orderId":{"type":"string","description":"Order id from `GET /api/v1/order-confirmations`; resolved to its project."},"categoryId":{"type":"string","description":"Defaults to the organization's default category."},"startTime":{"type":"string","format":"date-time"},"endTime":{"type":"string","format":"date-time"},"breakMinutes":{"type":"integer","minimum":0,"default":0},"description":{"type":"string"},"allowOverlap":{"type":"boolean","default":false,"description":"Save even if the entry overlaps an existing one for the same member. Without it, overlaps answer `409 conflict`."}}},"UpdateTimeEntryInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Send only what changes. Use either `projectId` or `orderId`, not both.","properties":{"projectId":{"type":"string"},"orderId":{"type":"string","description":"Order id from `GET /api/v1/order-confirmations`; resolved to its project."},"categoryId":{"type":"string","description":"An active category of this organization from `GET /api/v1/time-entry-categories`."},"startTime":{"type":"string","format":"date-time","description":"Also moves `date` when the day changes."},"endTime":{"type":"string","format":"date-time"},"breakMinutes":{"type":"integer","minimum":0},"description":{"type":["string","null"],"description":"`null` or an empty text removes the note."},"allowOverlap":{"type":"boolean","default":false,"description":"Save even if the entry overlaps an existing one for the same member. Without it, overlaps answer `409 conflict`. Does not count as a change on its own."}}},"TimeEntryDeleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"ExternalFormField":{"type":"object","additionalProperties":false,"required":["id","type","label","isRequired","position"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["text","address","email","phone","textarea","checkbox","multiselect","dropdown","date","number","file","datetime","time","radio","heading","notice","signature","sketch","table"]},"label":{"type":"string"},"placeholder":{"type":"string"},"helpText":{"type":"string"},"isRequired":{"type":"boolean"},"position":{"type":"number"},"options":{"description":"Select options or table column configuration, depending on the field type."}}},"ExternalForm":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","isActive","fields"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"isActive":{"type":"boolean"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFormField"}}}},"ExternalFormSubmissionAnswer":{"type":"object","additionalProperties":false,"required":["fieldId","label","type","value"],"properties":{"fieldId":{"type":"string"},"label":{"type":"string"},"type":{"type":"string","enum":["text","address","email","phone","textarea","checkbox","multiselect","dropdown","date","number","file","datetime","time","radio","heading","notice","signature","sketch","table","unknown"]},"value":{"description":"JSON value as submitted for this dynamic field."}}},"ExternalFormSubmission":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","formId","source","answers"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"formId":{"type":"string"},"customerId":{"type":"string"},"source":{"type":"string","enum":["web","api"]},"salesRoutingStatus":{"type":"string","enum":["queued","success","skipped","failed"]},"answers":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFormSubmissionAnswer"}}}},"CreateExternalFormSubmissionAnswer":{"type":"object","additionalProperties":false,"required":["fieldId","value"],"properties":{"fieldId":{"type":"string"},"value":{"description":"JSON value matching the form field type. File fields take an array\nof file ids from `POST /api/v1/files` (at most 5, each usable once);\nan empty array counts as \"no upload\".\n"}}},"CreateExternalFormSubmissionInput":{"type":"object","additionalProperties":false,"required":["formId","answers"],"properties":{"formId":{"type":"string"},"answers":{"type":"array","items":{"$ref":"#/components/schemas/CreateExternalFormSubmissionAnswer"}}}},"ExternalFormListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalForm"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"ExternalFormSubmissionResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/ExternalFormSubmission"}}},"ExternalFormSubmissionListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFormSubmission"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"FormSubmissionWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["form_submission.created"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["formSubmission"],"properties":{"formSubmission":{"$ref":"#/components/schemas/ExternalFormSubmission"}}}}},"InternalFormTemplate":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","isActive","fields"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"description":{"type":"string"},"isActive":{"type":"boolean"},"fields":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFormField"}}}},"FormInstanceContext":{"type":"object","additionalProperties":false,"required":["type","id"],"properties":{"type":{"type":"string","enum":["project","customer_request","maintenance"]},"id":{"type":"string"}}},"FormInstance":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","formTemplateId","status","accessType","context","pdfStatus"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"formTemplateId":{"type":"string"},"status":{"type":"string","enum":["pending","viewed","submitted"]},"accessType":{"type":"string","enum":["employee","customer"]},"context":{"oneOf":[{"$ref":"#/components/schemas/FormInstanceContext"},{"type":"null"}]},"shareUrl":{"type":"string","format":"uri","description":"Present only for customer-access instances. Anyone with this URL can open and submit the form."},"pdfStatus":{"type":"string","enum":["not_available","pending","ready","failed"]},"pdfUrl":{"type":"string","format":"uri","description":"Short-lived signed URL, present only while a ready PDF can be resolved."}}},"InternalFormSubmission":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","formTemplateId","formInstanceId","answers","pdfStatus"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"formTemplateId":{"type":"string"},"formInstanceId":{"type":"string"},"answers":{"type":"array","items":{"$ref":"#/components/schemas/ExternalFormSubmissionAnswer"}},"pdfStatus":{"type":"string","enum":["not_available","pending","ready","failed"]},"pdfUrl":{"type":"string","format":"uri","description":"Short-lived signed URL, present only while a ready PDF can be resolved."}}},"CreateFormInstanceInput":{"type":"object","additionalProperties":false,"required":["formTemplateId","contextType","contextId","sentTo"],"properties":{"formTemplateId":{"type":"string"},"contextType":{"type":"string","enum":["project","customer_request"],"description":"Maintenance protocol instances are created only by the maintenance workflow."},"contextId":{"type":"string"},"sentTo":{"type":"string","minLength":1,"description":"Recipient address or integration label recorded with the share link."},"customerId":{"type":"string"}}},"CreateInternalFormSubmissionInput":{"type":"object","additionalProperties":false,"required":["formInstanceId","answers"],"properties":{"formInstanceId":{"type":"string"},"answers":{"type":"array","items":{"$ref":"#/components/schemas/CreateExternalFormSubmissionAnswer"}}}},"InternalFormTemplateListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InternalFormTemplate"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"FormInstanceResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/FormInstance"}}},"FormInstanceListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/FormInstance"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"InternalFormSubmissionResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/InternalFormSubmission"}}},"InternalFormSubmissionListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InternalFormSubmission"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"FormInstanceCreatedWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["form_instance.created"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["formInstance"],"properties":{"formInstance":{"$ref":"#/components/schemas/FormInstance"}}}}},"FormInstanceSubmittedWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["form_instance.submitted"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["formInstanceSubmission"],"properties":{"formInstanceSubmission":{"$ref":"#/components/schemas/InternalFormSubmission"}}}}},"TimeEntryCreatedWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["time_entry.created"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["timeEntry"],"properties":{"timeEntry":{"oneOf":[{"$ref":"#/components/schemas/TimeEntry"},{"type":"object","additionalProperties":false,"description":"For an endpoint whose creator may not see this entry.","required":["id"],"properties":{"id":{"type":"string"}}}]}}}}},"TimeEntryChangeWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["time_entry.updated","time_entry.deleted"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["timeEntry"],"properties":{"timeEntry":{"oneOf":[{"$ref":"#/components/schemas/TimeEntry"},{"type":"object","additionalProperties":false,"description":"For an endpoint whose creator may not see this entry.","required":["id"],"properties":{"id":{"type":"string"}}}]}}}}},"PaymentCreatedWebhookDelivery":{"type":"object","description":"Fires for payments and for corrections, booked in craftivo or through\nthe API. Evaluate\n`payment.type`: a `correction` can carry a negative `amountCents`\nand is no incoming payment. Without the `financials` permission of\nthe endpoint's creator, the amounts of the payment and of the invoice\nstate are `null`.\n","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["payment.created"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["payment","invoice"],"properties":{"payment":{"$ref":"#/components/schemas/DocumentPayment"},"invoice":{"$ref":"#/components/schemas/DeliveredPaymentInvoiceState"}}}}},"Property":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"notes":{"type":"string"},"defaultAddressPartyId":{"type":"string","description":"Party whose address documents of this property are addressed to (Anschrift-Partei). Missing = the address of the project or customer."},"defaultDeliveryPartyId":{"type":"string","description":"Party whose email receives documents of this property (Versand-Partei), independent of the addressee."},"defaultQuoteRecipientPartyId":{"type":"string","description":"Default recipient party for quotes."},"defaultInvoiceRecipientPartyId":{"type":"string","description":"Default recipient party for invoices."}}},"PropertyDetail":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","name","customers","parties","projects"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"name":{"type":"string"},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"notes":{"type":"string"},"defaultAddressPartyId":{"type":"string"},"defaultDeliveryPartyId":{"type":"string"},"defaultQuoteRecipientPartyId":{"type":"string"},"defaultInvoiceRecipientPartyId":{"type":"string"},"customers":{"type":"array","description":"Customers linked through an active party, each once.","items":{"type":"object","additionalProperties":false,"required":["id"],"properties":{"id":{"type":"string"},"name":{"type":"string"}}}},"parties":{"type":"array","description":"All parties of the property, active ones first.","items":{"$ref":"#/components/schemas/Party"}},"projects":{"type":"array","description":"Projects and orders linked to the property, newest first. Archived\nand deleted ones are left out, and so are those the member who\ncreated the key may not see (same rule as `GET /api/v1/projects`).\n","items":{"type":"object","additionalProperties":false,"required":["id","name","type","status"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["project","order"]},"projectNumber":{"type":"string"},"status":{"type":"string"}}}}}},"Party":{"type":"object","additionalProperties":false,"required":["id","createdAt","updatedAt","propertyId","customerId","role","deliveryRouting","isActive","defaultFor"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"propertyId":{"type":"string"},"customerId":{"type":"string"},"customerName":{"type":"string"},"role":{"type":"string","enum":["hausverwaltung","weg","owner","tenant","hausmeister","facility_manager","haustechniker","architekt","energieberater","versicherung","eigentuemervertreter","sonstige"]},"roleLabel":{"type":"string","description":"Free label, only for role `sonstige`."},"deliveryRouting":{"type":"string","enum":["direct","via_hausverwaltung"],"description":"Whether documents go to the party itself or through the Hausverwaltung of the property."},"unitLabel":{"type":"string","description":"Unit within the building, e.g. \"Whg. 3 links\"."},"validFrom":{"type":"string","format":"date"},"validTo":{"type":"string","format":"date"},"isActive":{"type":"boolean"},"notes":{"type":"string"},"defaultFor":{"type":"array","description":"Default roles of this party on its property: `address`\n(Anschrift-Partei), `delivery` (Versand-Partei), `quote` and\n`invoice` (default recipients). Set them with\n`PATCH /api/v1/properties/{propertyId}`.\n","items":{"type":"string","enum":["address","delivery","quote","invoice"]}}}},"PropertyListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Property"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"PropertyDetailResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PropertyDetail"}}},"PartyListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Party"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"PartyResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Party"}}},"CreatePropertyInput":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"notes":{"type":"string"}}},"UpdatePropertyInput":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1},"externalId":{"type":"string"},"street":{"type":"string"},"houseNumber":{"type":"string"},"postalCode":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"notes":{"type":"string"},"defaultAddressPartyId":{"type":["string","null"],"description":"Active party of this property as Anschrift-Partei; null removes it."},"defaultDeliveryPartyId":{"type":["string","null"],"description":"Active party of this property as Versand-Partei; null removes it."},"defaultQuoteRecipientPartyId":{"type":["string","null"],"description":"Active party of this property as default quote recipient; null removes it."},"defaultInvoiceRecipientPartyId":{"type":["string","null"],"description":"Active party of this property as default invoice recipient; null removes it."}}},"CreatePartyInput":{"type":"object","additionalProperties":false,"required":["propertyId","customerId","role"],"properties":{"propertyId":{"type":"string"},"customerId":{"type":"string"},"role":{"type":"string","enum":["hausverwaltung","weg","owner","tenant","hausmeister","facility_manager","haustechniker","architekt","energieberater","versicherung","eigentuemervertreter","sonstige"]},"roleLabel":{"type":"string","description":"Only allowed with role `sonstige`."},"deliveryRouting":{"type":"string","enum":["direct","via_hausverwaltung"],"description":"Default `via_hausverwaltung` for role `weg`, otherwise `direct`."},"unitLabel":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validTo":{"type":"string","format":"date"},"isActive":{"type":"boolean","default":true},"notes":{"type":"string"}}},"UpdatePartyInput":{"type":"object","additionalProperties":false,"properties":{"customerId":{"type":"string"},"role":{"type":"string","enum":["hausverwaltung","weg","owner","tenant","hausmeister","facility_manager","haustechniker","architekt","energieberater","versicherung","eigentuemervertreter","sonstige"]},"roleLabel":{"type":"string","description":"Only allowed with role `sonstige`. Changing the role away from `sonstige` removes it."},"deliveryRouting":{"type":"string","enum":["direct","via_hausverwaltung"]},"unitLabel":{"type":"string"},"validFrom":{"type":"string","format":"date"},"validTo":{"type":"string","format":"date"},"isActive":{"type":"boolean","description":"false ends the party; it stays listed with `includeInactive=true`."},"notes":{"type":"string"}}},"PropertyDeleteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"PropertyWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["property.created","property.updated","property.deleted","property.restored"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["property"],"properties":{"property":{"$ref":"#/components/schemas/Property"}}}}},"ProjectWebhookDelivery":{"type":"object","additionalProperties":false,"required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["project.created","project.updated","project.deleted"]},"createdAt":{"type":"number"},"data":{"type":"object","additionalProperties":false,"required":["project"],"properties":{"project":{"$ref":"#/components/schemas/Project"}}}}},"DocumentRecipient":{"type":"object","additionalProperties":false,"required":["identityLines","addressLines","email","phone","vatId"],"properties":{"identityLines":{"type":"array","items":{"type":"string"}},"addressLines":{"type":"array","items":{"type":"string"}},"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"vatId":{"type":["string","null"]}}},"DocumentPosition":{"type":"object","additionalProperties":false,"required":["id","type","orderIndex","articleNumber","title","description","quantity","unit","unitPriceCents","netAmountCents","taxRatePercent","isOptional","positionLibraryId","sectionId","parentSectionId","subtotalCents"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["position","text","title","section","image"],"description":"`section` and `title` are section headers (Abschnitte). `title`\ncomes from older documents and behaves like `section`.\n"},"orderIndex":{"type":"number"},"articleNumber":{"type":["string","null"]},"title":{"type":["string","null"]},"description":{"type":["string","null"]},"quantity":{"type":["number","null"]},"unit":{"type":["string","null"]},"unitPriceCents":{"type":["integer","null"],"description":"`null` when the key's creator has neither the `financials` nor the `salesPrices` permission."},"netAmountCents":{"type":["integer","null"],"description":"`null` when the key's creator has neither the `financials` nor the `salesPrices` permission."},"taxRatePercent":{"type":["number","null"]},"isOptional":{"type":"boolean","description":"Optional item (Eventualposition). Not included in the document totals."},"positionLibraryId":{"type":["string","null"],"description":"Position library entry the line was taken from, see `GET /api/v1/positions`."},"sectionId":{"type":["string","null"],"description":"Id of the section header the row belongs to, as craftivo shows it.\nNull for section headers and for rows outside any section. Older\ndocuments without stored assignments get the section above the row.\n"},"parentSectionId":{"type":["string","null"],"description":"Only on section headers: the main section of a sub-section (one\nlevel deep). Null for main sections and all other rows.\n"},"subtotalCents":{"type":["integer","null"],"description":"Only on section headers: the section subtotal as shown in craftivo.\nNet of the rows in the section without optional items\n(Eventualpositionen), sub-sections included, before the document\ndiscount. When craftivo hides the single prices of the section\nand sets a lump sum (Pauschalpreis), the lump sum. Null on other\nrows and when the key's creator has neither the `financials` nor\nthe `salesPrices` permission.\n"}}},"Payment":{"type":"object","additionalProperties":false,"required":["id","type","amountCents","paidAt","method","skontoAppliedCents","skontoPercentApplied","reference","externalId","createdAt"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["payment","correction"],"description":"`correction` is a correction booking, made in craftivo or with\n`POST /api/v1/invoices/{invoiceId}/payments/corrections`; its\n`amountCents` can be negative.\n"},"amountCents":{"type":"integer"},"paidAt":{"type":"string","format":"date"},"method":{"type":"string","enum":["bank_transfer","cash","card","direct_debit","other"]},"skontoAppliedCents":{"type":["integer","null"],"description":"Skonto (early payment discount) settled with this payment, in cents. It counts towards the paid amount."},"skontoPercentApplied":{"type":["number","null"]},"reference":{"type":["string","null"]},"externalId":{"type":["string","null"],"description":"ID from the system that recorded the payment through the API; `null` for payments recorded in craftivo."},"createdAt":{"type":"string","format":"date-time","description":"When the payment was recorded in craftivo."}}},"DocumentPayment":{"type":"object","description":"A payment on a document and in `payment.created`. The amounts are `null` in webhook deliveries without the `financials` permission of the endpoint's creator.","additionalProperties":false,"required":["id","type","amountCents","paidAt","method","skontoAppliedCents","skontoPercentApplied","reference","externalId","createdAt"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["payment","correction"],"description":"`correction` is a correction booking, made in craftivo or with\n`POST /api/v1/invoices/{invoiceId}/payments/corrections`; its\n`amountCents` can be negative.\n"},"amountCents":{"type":["integer","null"],"description":"`null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"paidAt":{"type":"string","format":"date"},"method":{"type":"string","enum":["bank_transfer","cash","card","direct_debit","other"]},"skontoAppliedCents":{"type":["integer","null"],"description":"Skonto (early payment discount) settled with this payment, in cents. It counts towards the paid amount. `null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"skontoPercentApplied":{"type":["number","null"],"description":"`null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"reference":{"type":["string","null"]},"externalId":{"type":["string","null"],"description":"ID from the system that recorded the payment through the API; `null` for payments recorded in craftivo."},"createdAt":{"type":"string","format":"date-time","description":"When the payment was recorded in craftivo."}}},"PaymentRead":{"description":"A payment as you read it. The amounts need the `financials` permission of the key's creator; the payment stays readable without it.","allOf":[{"type":"object","required":["id","type","paidAt","method","reference","externalId","createdAt"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["payment","correction"],"description":"`correction` is a correction booking, made in craftivo or with\n`POST /api/v1/invoices/{invoiceId}/payments/corrections`; its\n`amountCents` can be negative.\n"},"paidAt":{"type":"string","format":"date"},"method":{"type":"string","enum":["bank_transfer","cash","card","direct_debit","other"]},"reference":{"type":["string","null"]},"externalId":{"type":["string","null"],"description":"ID from the system that recorded the payment through the API; `null` for payments recorded in craftivo."},"createdAt":{"type":"string","format":"date-time","description":"When the payment was recorded in craftivo."}}},{"type":"object","required":["amountCents","skontoAppliedCents","skontoPercentApplied"],"properties":{"amountCents":{"type":["integer","null"],"description":"`null` without the `financials` permission of the key's creator."},"skontoAppliedCents":{"type":["integer","null"],"description":"Skonto (early payment discount) settled with this payment, in cents. It counts towards the paid amount. `null` without the `financials` permission of the key's creator."},"skontoPercentApplied":{"type":["number","null"],"description":"`null` without the `financials` permission of the key's creator."}}}],"unevaluatedProperties":false},"PaymentInvoiceState":{"type":"object","additionalProperties":false,"required":["id","status","paidAmountCents","openAmountCents"],"properties":{"id":{"type":"string"},"status":{"type":"string","description":"Invoice status after the booking, e.g. `partially_paid` or `paid`."},"paidAmountCents":{"type":"integer","description":"Paid amount including skonto."},"openAmountCents":{"type":"integer","description":"What is still open on this invoice. 0 for an invoice draft, an archived, a cancelled or a superseded invoice."}}},"DeliveredPaymentInvoiceState":{"type":"object","additionalProperties":false,"description":"The invoice state in `payment.created`.","required":["id","status","paidAmountCents","openAmountCents"],"properties":{"id":{"type":"string"},"status":{"type":"string","description":"Invoice status after the booking, e.g. `partially_paid` or `paid`."},"paidAmountCents":{"type":["integer","null"],"description":"Paid amount including skonto. `null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"openAmountCents":{"type":["integer","null"],"description":"What is still open on this invoice. 0 for an invoice draft, an archived, a cancelled or a superseded invoice. `null` in webhook deliveries without the `financials` permission of the endpoint's creator."}}},"PaymentCreateRequest":{"type":"object","additionalProperties":true,"required":["amountCents","paidAt","method","externalId"],"properties":{"amountCents":{"type":"integer","minimum":1,"description":"Amount received, in cents. Never a decimal amount."},"paidAt":{"type":"string","format":"date","description":"Day the payment arrived, `YYYY-MM-DD`."},"method":{"type":"string","enum":["bank_transfer","cash","card","direct_debit","other"]},"externalId":{"type":"string","minLength":1,"maxLength":255,"description":"Your ID of this payment, e.g. the bank transaction ID. Unique per\ninvoice: a collective transfer may carry the same ID on several\ninvoices. Repeating a request with the same `externalId` never\nbooks twice.\n"},"skontoAppliedCents":{"type":"integer","minimum":0,"description":"Skonto (early payment discount) the customer deducted, in cents.\nIt is not a payment method; it settles part of the invoice\ntogether with `amountCents`.\n"},"skontoPercentApplied":{"type":"number","minimum":0,"maximum":100},"reference":{"type":"string","description":"Reference text, e.g. the purpose line of the bank transfer."},"note":{"type":"string","description":"Internal note, visible in craftivo."}}},"InvoiceCancelRequest":{"type":"object","additionalProperties":false,"required":["reason"],"properties":{"reason":{"type":"string","minLength":10,"maxLength":1000,"description":"Why the invoice is cancelled. Visible in craftivo and in the invoice history.","example":"Dachfläche falsch aufgemessen, Rechnung wird neu gestellt"}}},"PaymentCorrectionCreateRequest":{"type":"object","additionalProperties":true,"required":["amountCents","paidAt","note","externalId"],"properties":{"amountCents":{"type":"integer","maximum":-1,"description":"Amount to reverse, in cents, as a negative number. Never a decimal amount."},"paidAt":{"type":"string","format":"date","description":"Day of the correction, `YYYY-MM-DD`."},"note":{"type":"string","minLength":1,"description":"Reason for the correction. Required. Visible in craftivo and in the invoice history."},"externalId":{"type":"string","minLength":1,"maxLength":255,"description":"Your ID of this correction. Unique per invoice, together with the\n`externalId` of its payments. Repeating a request with the same\n`externalId` never books twice.\n"},"method":{"type":"string","enum":["bank_transfer","cash","card","direct_debit","other"],"default":"other"},"reference":{"type":"string","description":"Reference text, e.g. the ID of the reversal in your system."}}},"PaymentRecordResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"description":"The payment, as in `Payment`, plus the invoice state and warnings.","required":["id","type","amountCents","paidAt","method","skontoAppliedCents","skontoPercentApplied","reference","externalId","createdAt","invoice","warnings"],"properties":{"id":{"$ref":"#/components/schemas/Payment/properties/id"},"type":{"$ref":"#/components/schemas/Payment/properties/type"},"amountCents":{"$ref":"#/components/schemas/Payment/properties/amountCents"},"paidAt":{"$ref":"#/components/schemas/Payment/properties/paidAt"},"method":{"$ref":"#/components/schemas/Payment/properties/method"},"skontoAppliedCents":{"$ref":"#/components/schemas/Payment/properties/skontoAppliedCents"},"skontoPercentApplied":{"$ref":"#/components/schemas/Payment/properties/skontoPercentApplied"},"reference":{"$ref":"#/components/schemas/Payment/properties/reference"},"externalId":{"$ref":"#/components/schemas/Payment/properties/externalId"},"createdAt":{"$ref":"#/components/schemas/Payment/properties/createdAt"},"invoice":{"$ref":"#/components/schemas/PaymentInvoiceState"},"warnings":{"type":"array","description":"Accepted, but worth a look: `skonto_after_deadline` (skonto\nafter the skonto deadline of the invoice),\n`skonto_not_agreed` (the invoice has no skonto terms) or\n`skonto_exceeds_terms` (more skonto than the agreed rate of\nthe invoice allows; 1 cent of rounding is accepted).\n","items":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string","enum":["skonto_after_deadline","skonto_not_agreed","skonto_exceeds_terms"]},"message":{"type":"string"}}}}}}}},"PaymentListResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Payment"}}}},"DocumentFields":{"type":"object","required":["id","createdAt","updatedAt","number","title","status","finalizedAt","sentAt","acceptedAt","rejectedAt","bookedAt","paidAt","overdueAt","cancelledAt","customerId","projectId","date","dueDate","validUntil","netAmountCents","taxAmountCents","grossAmountCents","taxRatePercent","currency","payments","recipient","notes","pdfUrl","relatedInvoiceId","supersededByFinalInvoiceId"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"number":{"type":["string","null"]},"title":{"type":["string","null"]},"status":{"type":"string","enum":["draft","finalized","sent","customer_review","accepted","rejected","superseded","booked","partially_paid","overdue","paid","cancelled","archived"]},"finalizedAt":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"acceptedAt":{"type":["string","null"],"format":"date-time"},"rejectedAt":{"type":["string","null"],"format":"date-time"},"bookedAt":{"type":["string","null"],"format":"date-time"},"paidAt":{"type":["string","null"],"format":"date-time"},"overdueAt":{"type":["string","null"],"format":"date-time"},"cancelledAt":{"type":["string","null"],"format":"date-time"},"customerId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"date":{"type":"string","format":"date"},"dueDate":{"type":["string","null"],"format":"date"},"validUntil":{"type":["string","null"],"format":"date"},"netAmountCents":{"type":["integer","null"],"description":"`null` in the response to `PATCH /api/v1/quotes/{quoteId}` and in webhook deliveries when the creator of the key or of the endpoint has neither the `financials` nor the `salesPrices` permission."},"taxAmountCents":{"type":["integer","null"],"description":"`null` in the response to `PATCH /api/v1/quotes/{quoteId}` and in webhook deliveries when the creator of the key or of the endpoint has neither the `financials` nor the `salesPrices` permission."},"grossAmountCents":{"type":["integer","null"],"description":"`null` in the response to `PATCH /api/v1/quotes/{quoteId}` and in webhook deliveries when the creator of the key or of the endpoint has neither the `financials` nor the `salesPrices` permission."},"taxRatePercent":{"type":["number","null"]},"currency":{"type":"string","enum":["EUR"]},"payments":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPayment"},"description":"All payments recorded on the document, oldest first. Always\npresent in both list and detail responses so integrations can\niterate without existence checks; document types that never\ncarry payments (quotes, cost estimates, order confirmations,\ndelivery notes) always return an empty array. Entries with\n`type: correction` are GoBD-compliant counter-entries with a\nnegative `amountCents` — always sum payments sign-aware, never\ncount entries or add absolute values. The entries stay in webhook deliveries without the `financials` permission of the endpoint's creator,\nbut their amounts are `null`.\n"},"recipient":{"oneOf":[{"$ref":"#/components/schemas/DocumentRecipient"},{"type":"null"}]},"notes":{"type":["string","null"]},"pdfUrl":{"type":["string","null"],"format":"uri","description":"`null` until the archived PDF exists. The PDF shows all amounts, so this is also `null` in webhook deliveries when the endpoint's creator has neither the `financials` nor the `salesPrices` permission."},"relatedInvoiceId":{"type":["string","null"]},"supersededByFinalInvoiceId":{"type":["string","null"]}}},"Quote":{"allOf":[{"$ref":"#/components/schemas/DocumentFields"}],"unevaluatedProperties":false},"CostEstimate":{"allOf":[{"$ref":"#/components/schemas/DocumentFields"}],"unevaluatedProperties":false},"OrderConfirmation":{"allOf":[{"$ref":"#/components/schemas/DocumentFields"}],"unevaluatedProperties":false},"DeliveryNote":{"allOf":[{"$ref":"#/components/schemas/DocumentFields"}],"unevaluatedProperties":false},"Retention":{"type":"object","required":["id","invoiceId","invoiceNumber","kind","label","amountCents","paidCents","dueDate","state","releaseReason","forfeitNote"],"properties":{"id":{"type":"string"},"invoiceId":{"type":"string"},"invoiceNumber":{"type":["string","null"]},"kind":{"type":"string","enum":["security_retention","warranty_retention"]},"label":{"type":"string"},"amountCents":{"type":"integer"},"paidCents":{"type":"integer","description":"Paid on the released amount so far."},"dueDate":{"type":"string","format":"date"},"state":{"type":"string","description":"`retained` is not open and never overdue. `released` is open again. Only these two appear in the list. `forfeited`: the customer keeps the retention for good; craftivo books no tax correction.","enum":["retained","released","paid_out","settled_by_final","cancelled","forfeited"]},"releaseReason":{"type":["string","null"],"enum":["due","guarantee",null]},"forfeitNote":{"type":["string","null"],"description":"Why the customer keeps the retention, for `forfeited`."}}},"InvoiceDeduction":{"type":"object","required":["kind","label","beforeTax","amountCents"],"properties":{"kind":{"type":"string","enum":["security_retention","warranty_retention","site_utilities","construction_insurance"]},"label":{"type":"string","description":"Text as printed on the invoice."},"beforeTax":{"type":"boolean"},"amountCents":{"type":"integer"}}},"InvoiceServicePeriod":{"type":"object","additionalProperties":false,"required":["from","to"],"description":"Service period of an invoice, first and last day. A bound is `null` only\nwhen someone entered half a period in craftivo. Such a period is not\nprinted on the invoice; finalizing turns the date it has into the\nservice date, with the warning `service_date_from_partial_period`.\n","properties":{"from":{"type":["string","null"],"format":"date"},"to":{"type":["string","null"],"format":"date"}}},"InvoiceFields":{"allOf":[{"$ref":"#/components/schemas/DocumentFields"},{"type":"object","required":["type","paidAmountCents","openAmountCents","isOverdue","overdueDays"],"properties":{"type":{"type":"string","enum":["standard","partial","final","down_payment","credit_note"]},"paidAmountCents":{"type":["integer","null"],"description":"`null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"openAmountCents":{"type":["integer","null"],"description":"What is still open on this invoice. 0 for an invoice draft (`draft`), an archived (`archived`), a cancelled (`cancelled`) or a superseded invoice (`supersededByFinalInvoiceId` set), so the sum per customer is the open receivable. `null` in webhook deliveries without the `financials` permission of the endpoint's creator."},"payableAmountCents":{"type":["integer","null"],"description":"What the customer pays on this invoice: gross minus deducted partial invoices and deductions after gross. `grossAmountCents` stays unchanged by retentions. `null` in webhook deliveries when the endpoint's creator has neither the `financials` nor the `salesPrices` permission."},"deductions":{"type":["array","null"],"description":"Deductions at the end of the invoice. Those with `beforeTax` already reduce net, tax and gross. `null` in webhook deliveries when the endpoint's creator has neither the `financials` nor the `salesPrices` permission.","items":{"$ref":"#/components/schemas/InvoiceDeduction"}},"isOverdue":{"type":"boolean","description":"`true` when something is still open after the due date. A partially paid invoice keeps `status` `partially_paid` and gets `isOverdue` `true`; a later partial payment keeps it."},"overdueDays":{"type":"integer","minimum":0,"description":"Days since the due date while `isOverdue` is `true`, otherwise 0."},"serviceDate":{"type":["string","null"],"format":"date","description":"The service date of the invoice. `null` when it has a service period or no service time."},"servicePeriod":{"description":"The service period of the invoice. `null` when it has a service date or no service time.","oneOf":[{"$ref":"#/components/schemas/InvoiceServicePeriod"},{"type":"null"}]}}}]},"Invoice":{"allOf":[{"$ref":"#/components/schemas/InvoiceFields"}],"unevaluatedProperties":false},"DocumentReadFields":{"type":"object","required":["id","createdAt","updatedAt","number","title","status","finalizedAt","sentAt","acceptedAt","rejectedAt","bookedAt","paidAt","overdueAt","cancelledAt","customerId","projectId","date","dueDate","validUntil","netAmountCents","taxAmountCents","grossAmountCents","taxRatePercent","currency","payments","recipient","notes","pdfUrl","relatedInvoiceId","supersededByFinalInvoiceId"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"number":{"type":["string","null"]},"title":{"type":["string","null"]},"status":{"type":"string","enum":["draft","finalized","sent","customer_review","accepted","rejected","superseded","booked","partially_paid","overdue","paid","cancelled","archived"]},"finalizedAt":{"type":["string","null"],"format":"date-time"},"sentAt":{"type":["string","null"],"format":"date-time"},"acceptedAt":{"type":["string","null"],"format":"date-time"},"rejectedAt":{"type":["string","null"],"format":"date-time"},"bookedAt":{"type":["string","null"],"format":"date-time"},"paidAt":{"type":["string","null"],"format":"date-time"},"overdueAt":{"type":["string","null"],"format":"date-time"},"cancelledAt":{"type":["string","null"],"format":"date-time"},"customerId":{"type":["string","null"]},"projectId":{"type":["string","null"]},"date":{"type":"string","format":"date"},"dueDate":{"type":["string","null"],"format":"date"},"validUntil":{"type":["string","null"],"format":"date"},"netAmountCents":{"type":["integer","null"],"description":"`null` on `GET` and in write responses when the key's creator has neither the `financials` nor the `salesPrices` permission."},"taxAmountCents":{"type":["integer","null"],"description":"`null` on `GET` and in write responses when the key's creator has neither the `financials` nor the `salesPrices` permission."},"grossAmountCents":{"type":["integer","null"],"description":"`null` on `GET` and in write responses when the key's creator has neither the `financials` nor the `salesPrices` permission."},"taxRatePercent":{"type":["number","null"]},"currency":{"type":"string","enum":["EUR"]},"payments":{"type":"array","items":{"$ref":"#/components/schemas/PaymentRead"},"description":"All payments recorded on the document, oldest first. Always\npresent in both list and detail responses so integrations can\niterate without existence checks; document types that never\ncarry payments (quotes, cost estimates, order confirmations,\ndelivery notes) always return an empty array. Entries with\n`type: correction` are GoBD-compliant counter-entries with a\nnegative `amountCents` — always sum payments sign-aware, never\ncount entries or add absolute values. Without the `financials`\npermission of the key's creator the entries stay, but their amounts\nare `null`.\n"},"recipient":{"oneOf":[{"$ref":"#/components/schemas/DocumentRecipient"},{"type":"null"}]},"notes":{"type":["string","null"]},"pdfUrl":{"type":["string","null"],"format":"uri","description":"`null` until the archived PDF exists. The PDF shows all amounts, so this is also `null` on `GET` and in the response of a write route, including a replay, when the key's creator has neither the `financials` nor the `salesPrices` permission."},"relatedInvoiceId":{"type":["string","null"]},"supersededByFinalInvoiceId":{"type":["string","null"]}}},"QuoteRead":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"}],"unevaluatedProperties":false},"CostEstimateRead":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"}],"unevaluatedProperties":false},"OrderConfirmationRead":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"}],"unevaluatedProperties":false},"DeliveryNoteRead":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"}],"unevaluatedProperties":false},"InvoiceReadFields":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"},{"type":"object","required":["type","paidAmountCents","openAmountCents","isOverdue","overdueDays"],"properties":{"type":{"type":"string","enum":["standard","partial","final","down_payment","credit_note"]},"payableAmountCents":{"type":["integer","null"],"description":"What the customer pays on this invoice: gross minus deducted partial invoices and deductions after gross. `null` when the key's creator has neither the `financials` nor the `salesPrices` permission."},"deductions":{"type":["array","null"],"description":"Deductions at the end of the invoice. Those with `beforeTax` already reduce net, tax and gross. `null` when the key's creator has neither the `financials` nor the `salesPrices` permission.","items":{"$ref":"#/components/schemas/InvoiceDeduction"}},"paidAmountCents":{"type":["integer","null"],"description":"`null` without the `financials` permission of the key's creator; `salesPrices` alone does not show payment data."},"openAmountCents":{"type":["integer","null"],"description":"What is still open on this invoice. 0 for an invoice draft (`draft`), an archived (`archived`), a cancelled (`cancelled`) or a superseded invoice (`supersededByFinalInvoiceId` set), so the sum per customer is the open receivable. `null` without the `financials` permission of the key's creator; `salesPrices` alone does not show payment data."},"isOverdue":{"type":"boolean","description":"`true` when something is still open after the due date. A partially paid invoice keeps `status` `partially_paid` and gets `isOverdue` `true`; a later partial payment keeps it."},"overdueDays":{"type":"integer","minimum":0,"description":"Days since the due date while `isOverdue` is `true`, otherwise 0."},"serviceDate":{"type":["string","null"],"format":"date","description":"The service date of the invoice. `null` when it has a service period or no service time."},"servicePeriod":{"description":"The service period of the invoice. `null` when it has a service date or no service time.","oneOf":[{"$ref":"#/components/schemas/InvoiceServicePeriod"},{"type":"null"}]}}}]},"InvoiceRead":{"allOf":[{"$ref":"#/components/schemas/InvoiceReadFields"}],"unevaluatedProperties":false},"DocumentWebhookData":{"description":"The public list representation of the document that caused the event.\nExactly one singular resource key is present.\n","oneOf":[{"type":"object","additionalProperties":false,"required":["quote"],"properties":{"quote":{"$ref":"#/components/schemas/Quote"}}},{"type":"object","additionalProperties":false,"required":["cost_estimate"],"properties":{"cost_estimate":{"$ref":"#/components/schemas/CostEstimate"}}},{"type":"object","additionalProperties":false,"required":["order_confirmation"],"properties":{"order_confirmation":{"$ref":"#/components/schemas/OrderConfirmation"}}},{"type":"object","additionalProperties":false,"required":["delivery_note"],"properties":{"delivery_note":{"$ref":"#/components/schemas/DeliveryNote"}}},{"type":"object","additionalProperties":false,"required":["invoice"],"properties":{"invoice":{"$ref":"#/components/schemas/Invoice"}}}]},"DocumentWebhookDelivery":{"type":"object","additionalProperties":false,"description":"Delivery body for quote, cost estimate, order confirmation, delivery note, and invoice events.","required":["id","event","createdAt","data"],"properties":{"id":{"type":"string"},"event":{"type":"string","enum":["quote.created","quote.updated","quote.finalized","quote.sent","quote.accepted","quote.rejected","cost_estimate.created","cost_estimate.updated","cost_estimate.finalized","cost_estimate.sent","cost_estimate.accepted","cost_estimate.rejected","order_confirmation.created","order_confirmation.updated","order_confirmation.finalized","order_confirmation.sent","order_confirmation.accepted","order_confirmation.rejected","delivery_note.created","delivery_note.updated","delivery_note.finalized","delivery_note.sent","delivery_note.accepted","delivery_note.rejected","invoice.created","invoice.updated","invoice.finalized","invoice.sent","invoice.accepted","invoice.rejected","invoice.paid","invoice.partially_paid","invoice.overdue","invoice.cancelled"]},"createdAt":{"type":"integer","format":"int64","description":"Unix timestamp in milliseconds."},"data":{"$ref":"#/components/schemas/DocumentWebhookData"}}},"DocumentDetailFields":{"type":"object","required":["introductionText","conclusionText","recipientMode","deliveryEmail","deliveryEmailOverride","deliveryPartyId","version"],"properties":{"introductionText":{"type":["string","null"]},"conclusionText":{"type":["string","null"]},"recipientMode":{"type":["string","null"],"enum":["customer","property_default","property_party","manual",null],"description":"Who the document is addressed to. Null for documents saved before recipient modes existed."},"deliveryEmail":{"type":["string","null"],"description":"Email address the document is sent to, as currently resolved."},"deliveryEmailOverride":{"type":["string","null"],"description":"Email address set on this document that replaces the default delivery address."},"deliveryPartyId":{"type":["string","null"],"description":"Property party set on this document as delivery recipient."},"version":{"type":"integer","minimum":1,"description":"Grows by one with every save, in craftivo and through the API.\nSend it with `PATCH /api/v1/quotes/{quoteId}` so a change made in\nthe meantime is not overwritten.\n"}}},"QuoteDetail":{"allOf":[{"$ref":"#/components/schemas/DocumentReadFields"},{"$ref":"#/components/schemas/DocumentDetailFields"},{"type":"object","required":["positions"],"properties":{"positions":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPosition"}}}}],"unevaluatedProperties":false},"CostEstimateDetail":{"allOf":[{"$ref":"#/components/schemas/QuoteDetail"}],"unevaluatedProperties":false},"OrderConfirmationDetail":{"allOf":[{"$ref":"#/components/schemas/QuoteDetail"}],"unevaluatedProperties":false},"DeliveryNoteDetail":{"allOf":[{"$ref":"#/components/schemas/QuoteDetail"}],"unevaluatedProperties":false},"InvoiceDetail":{"allOf":[{"$ref":"#/components/schemas/InvoiceReadFields"},{"$ref":"#/components/schemas/DocumentDetailFields"},{"type":"object","required":["positions"],"properties":{"positions":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPosition"}},"settlement":{"description":"Only on final invoices. `null` without the `financials` permission of the key's creator (`salesPrices` alone is not enough, the rows name the payment state of other invoices) or if the key cannot read the project with `GET /projects/{projectId}`.","oneOf":[{"$ref":"#/components/schemas/InvoiceSettlement"},{"type":"null"}]},"progressBilling":{"description":"Only on partial invoices by progress (created with `partialInvoice.progress` or in craftivo with \"Stand bis heute\"). `null` without the `financials` permission of the key's creator (`salesPrices` alone is not enough, the rows name the payment state of other invoices) or if the key cannot read the project with `GET /projects/{projectId}`.","oneOf":[{"$ref":"#/components/schemas/InvoiceProgressBilling"},{"type":"null"}]}}}],"unevaluatedProperties":false},"InvoiceFinalized":{"allOf":[{"$ref":"#/components/schemas/InvoiceReadFields"},{"$ref":"#/components/schemas/DocumentDetailFields"},{"type":"object","required":["positions"],"properties":{"positions":{"type":"array","items":{"$ref":"#/components/schemas/DocumentPosition"}},"settlement":{"$ref":"#/components/schemas/InvoiceSettlement","description":"Only on final invoices."},"progressBilling":{"$ref":"#/components/schemas/InvoiceProgressBilling","description":"Only on partial invoices by progress, frozen at finalizing."},"serviceDate":{"type":["string","null"],"format":"date","description":"The service date of the invoice. `null` when it has a service period instead."},"warnings":{"type":"array","description":"Finalized, but worth a look: `service_date_defaulted` (the\ninvoice had no date at all, so the invoice date applies as the\nservice date; never for partial or down payment invoices) or `service_date_from_partial_period` (the invoice\nhad only the start or only the end of a service period, so that\ndate applies as the service date).\n","items":{"type":"object","additionalProperties":false,"required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}}],"unevaluatedProperties":false},"QuotePositionInput":{"description":"A row is a section header (`type: section`), a library position or a\nfree position. A position belongs to a section with `sectionId`.\nWhen a request sends section headers or `sectionId`, its rows follow\nthe order of the document: a section opens with its row; a position\nbelongs to the open section or to its main section; a sub-section\n(`parentSectionId`) follows its main section, one level deep; a\nposition without `sectionId` closes the open section. When a document\nhas sections, a position after a section needs `sectionId`. Anything\nelse is rejected with 400, never moved silently.\nA request without section headers and without `sectionId` is not\nchecked for sections; its positions are placed as in craftivo. If no\nposition of the document has a `sectionId` yet, for example when it\nhas only section headers or is an older document, appended positions\nbelong to the last section. Otherwise they stand outside the sections.\n","oneOf":[{"type":"object","title":"Section","additionalProperties":false,"required":["type","title"],"description":"A section header (Abschnitt) without a price. Its subtotal follows\nfrom its rows, see `subtotalCents` in the response. Lump sums\n(Pauschalpreis) are set in craftivo only.\n","properties":{"type":{"type":"string","enum":["section"]},"id":{"type":"string","maxLength":100,"description":"Your own key for the section, unique within the request, so\npositions in the same request can point to it with\n`sectionId`. The response returns the real id. In\n`appendPositions` it must not equal the id of an existing row.\n"},"title":{"type":"string","minLength":1},"parentSectionId":{"type":"string","description":"Makes the section a sub-section of the main section directly\nabove: its `id` from this request or, in `appendPositions`,\nthe real id of the last open main section.\n"}}},{"type":"object","title":"Library position","additionalProperties":false,"required":["quantity"],"oneOf":[{"required":["positionId"]},{"required":["articleNumber"]}],"description":"A position from the position library, found by `positionId` or by\n`articleNumber` (exactly one of both). Title, description, unit,\nprice and calculation come from the library as if the position\nhad been picked in craftivo. With a calculation the unit price\ndepends on the quantity; never compute it as list price times\nquantity.\n","properties":{"type":{"type":"string","enum":["position"],"default":"position"},"positionId":{"type":"string"},"articleNumber":{"type":"string"},"sectionId":{"type":"string","description":"`id` of a section in this request or, in `appendPositions`, the\nreal id of an existing section. See the order rules above.\n"},"quantity":{"type":"number","exclusiveMinimum":0},"isOptional":{"type":"boolean","description":"Optional item (Eventualposition), not part of the totals."},"vatRatePercent":{"type":"number","description":"Only in Switzerland, where positions may carry their own rate.\nElsewhere it must equal the rate of the organization.\n"}}},{"type":"object","title":"Free position","additionalProperties":false,"required":["title","quantity","unit","unitPriceCents"],"properties":{"type":{"type":"string","enum":["position"],"default":"position"},"title":{"type":"string","minLength":1},"description":{"type":"string"},"sectionId":{"type":"string","description":"Same as for library positions."},"quantity":{"type":"number","exclusiveMinimum":0},"unit":{"type":"string","minLength":1},"unitPriceCents":{"type":"integer"},"isOptional":{"type":"boolean"},"vatRatePercent":{"type":"number"}}}]},"CreateQuoteInput":{"type":"object","additionalProperties":false,"required":["projectId"],"properties":{"projectId":{"type":"string","description":"Project the quote belongs to. craftivo edits documents only within a project."},"customerId":{"type":"string","description":"The customer comes from the project. If sent, it must match the\nproject's customer. Required when the project has no customer.\n"},"title":{"type":"string"},"introductionText":{"type":"string","description":"Omitted = the organization's default introduction for quotes. Empty string = no text."},"conclusionText":{"type":"string","description":"Omitted = the organization's default closing text for quotes. Empty string = no text."},"vatOptionId":{"type":"string","description":"VAT option of the organization's country, for example\n`vat_de_zero_reverse_charge_general`. Omitted = the organization's default.\nSmall businesses (Kleinunternehmer) cannot pick an option that\ncharges VAT.\n"},"recipientMode":{"type":"string","enum":["customer","manual","property_default"],"default":"customer","description":"`manual` and `property_default` need property management enabled.\n`property_default` uses the property of the project and fails when\nthe project has no property, the property was deleted or its\ndefault quote recipient is no longer active.\n"},"manualRecipient":{"type":"object","additionalProperties":false,"required":["identityLines","addressLines"],"description":"Required with `recipientMode: manual`.","properties":{"identityLines":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string"}},"addressLines":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string"}},"email":{"type":"string","format":"email"},"phone":{"type":"string"}}},"deliveryEmailOverride":{"type":"string","format":"email","description":"Sends the document to this address instead of the recipient's. Needs property management enabled."},"positions":{"type":"array","maxItems":500,"items":{"$ref":"#/components/schemas/QuotePositionInput"}}}},"CreateInvoiceInput":{"type":"object","additionalProperties":false,"required":["source"],"properties":{"source":{"type":"object","additionalProperties":false,"required":["type","id"],"description":"The finalized quote or order confirmation, or the maintenance\nvisit (`maintenance_visit`, id from `GET /api/v1/maintenance-visits`)\nthe invoice is created from. `project` is only valid for a partial\ninvoice with mode `free_amount`.\n","properties":{"type":{"type":"string","enum":["quote","order_confirmation","maintenance_visit","project"]},"id":{"type":"string","description":"Id of the quote, order confirmation, maintenance visit or project."}}},"invoiceType":{"type":"string","description":"`standard` (default), or `partial` for a partial invoice. `down_payment`\nmeans the same as `partial`: the invoice is stored and returned with\ntype `partial`, because only partial invoices have these modes and\nlocks in craftivo. `final` creates a final invoice that deducts the\nearlier partial invoices. Credit notes are not available via the API\nyet (400 `unsupported_invoice_type`).\n","enum":["standard","partial","down_payment","final"]},"partialInvoice":{"$ref":"#/components/schemas/CreatePartialInvoiceInput"},"finalInvoice":{"$ref":"#/components/schemas/CreateFinalInvoiceInput"},"serviceDate":{"type":"string","format":"date","description":"Service date (§ 14 UStG), a calendar date as `YYYY-MM-DD`. Send either\n`serviceDate` or `servicePeriod`, not both (400 `invalid_body`). It\nreplaces a service date or period the invoice has so far. It may be\nbefore or after the invoice date.\n"},"servicePeriod":{"type":"object","additionalProperties":false,"required":["from","to"],"description":"Service period (§ 14 UStG), both days as `YYYY-MM-DD`. `from` must not\nbe after `to`; the same day is allowed. Half a period answers 400\n`invalid_body`. Replaces a service date or period the invoice has so far.\n","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}}}},"CreateFinalInvoiceInput":{"type":"object","additionalProperties":false,"description":"Optional with `invoiceType` `final`, otherwise not allowed.","properties":{"offsetScope":{"type":"string","enum":["source","project"],"description":"Which partial invoices the final invoice may deduct, as in the dialog\nin craftivo. Only applies when the project has more than one order:\n`source` (default) deducts those of this order and those on its quote,\nif that quote has no other active order. `project` deducts those of\nthe whole project. With one order craftivo decides and ignores this\nvalue: `project` if partial invoices exist outside that order and its\nquote, otherwise `source`. `settlement.offsetScope` shows the result.\n"}}},"InvoiceSettlement":{"type":"object","additionalProperties":false,"required":["offsetScope","offsets","deductedCents","payableCents"],"description":"How a final invoice settles the earlier partial invoices. On a draft this\nis what finalizing would deduct today; after finalizing it is frozen.\nThe invoice's own amounts stay the full service.\n","properties":{"offsetScope":{"type":"string","enum":["source","project"]},"offsets":{"type":"array","description":"One entry per partial invoice that is finalized, not cancelled and not\ndeducted by another final invoice. As in craftivo the amount paid on a\npartial invoice reduces the final invoice, at most its gross amount. A\npartly paid one counts with the amount paid; the rest stays in the final\ninvoice and the partial invoice is replaced. On a draft, unpaid ones are\nlisted with `appliedDeductionCents` 0; finalizing does not keep them.\n","items":{"type":"object","additionalProperties":false,"required":["invoiceId","number","date","netAmountCents","taxAmountCents","grossAmountCents","taxRatePercent","paymentStatus","paidAt","appliedDeductionCents","appliedNetCents","appliedTaxCents","appliedByTaxRate"],"properties":{"invoiceId":{"type":"string"},"number":{"type":"string"},"date":{"type":"string","description":"Invoice date of the partial invoice."},"netAmountCents":{"type":"integer"},"taxAmountCents":{"type":"integer"},"grossAmountCents":{"type":"integer"},"taxRatePercent":{"type":"number"},"paymentStatus":{"type":"string","enum":["paid","partially_paid","unpaid"],"description":"Payment state of the partial invoice. `partially_paid` is new; treat an unknown value like `unpaid`."},"paidAt":{"type":["string","null"],"description":"Date of the last payment, also for `partially_paid`."},"appliedDeductionCents":{"type":"integer","description":"Gross amount deducted. At most the amount paid, so it can be lower than `grossAmountCents`, for example after a partial payment or a cash discount."},"appliedNetCents":{"type":"integer","description":"Net share of the deduction, as printed on the PDF."},"appliedTaxCents":{"type":"integer","description":"Tax share of the deduction, as printed on the PDF."},"appliedByTaxRate":{"type":"array","description":"The deduction per tax rate of the partial invoice. One entry with a single rate, otherwise one per rate. The entries add up exactly to `appliedDeductionCents`, `appliedNetCents` and `appliedTaxCents`.","items":{"type":"object","additionalProperties":false,"required":["taxRatePercent","appliedDeductionCents","appliedNetCents","appliedTaxCents"],"properties":{"taxRatePercent":{"type":"number"},"appliedDeductionCents":{"type":"integer"},"appliedNetCents":{"type":"integer"},"appliedTaxCents":{"type":"integer"}}}}}}},"deductedCents":{"type":"integer","description":"Sum of `appliedDeductionCents`."},"payableCents":{"type":"integer","description":"Remaining amount of the final invoice, gross, never below 0."}}},"InvoiceProgressBilling":{"type":"object","additionalProperties":false,"required":["progressNetCents","deductedInvoices","deductedNetCents","payableCents"],"description":"How a partial invoice by progress (VOB/B § 16) is calculated: the\nprogress to date, net, minus the net amount of every finalized partial\nand down payment invoice of the order that is not cancelled, paid or\nnot. Tax is only charged on the difference. The invoice's own\n`netAmountCents`, `taxAmountCents` and `grossAmountCents` are that\ndifference, as printed on the PDF. On a draft this is what finalizing\nwould deduct today, and the invoice's amounts in the same response are\ncalculated with the same invoices; after finalizing it is frozen.\n","properties":{"progressNetCents":{"type":"integer","description":"Net amount of all positions at their progress to date, after the discount of the order, before deducting earlier invoices."},"deductedInvoices":{"type":"array","description":"One entry per deducted partial or down payment invoice.","items":{"type":"object","additionalProperties":false,"required":["invoiceId","number","date","netAmountCents","paymentStatus","paidAt"],"properties":{"invoiceId":{"type":"string"},"number":{"type":"string"},"date":{"type":"string","description":"Invoice date of the deducted invoice."},"netAmountCents":{"type":"integer","description":"Net amount deducted, the full net amount of that invoice."},"paymentStatus":{"type":"string","enum":["paid","partially_paid","unpaid"],"description":"Payment state of the deducted invoice. It is deducted in full in every state. Treat an unknown value like `unpaid`."},"paidAt":{"type":["string","null"],"description":"Date of the last payment, also for `partially_paid`."}}}},"deductedNetCents":{"type":"integer","description":"Sum of `netAmountCents` of `deductedInvoices`."},"payableCents":{"type":"integer","description":"What the customer pays on this invoice, gross, after retentions.\nBelow 0 when the progress is not above the earlier invoices; such\na draft cannot be finalized (409 `conflict`).\n"}}},"CreatePartialInvoiceInput":{"type":"object","additionalProperties":false,"required":["mode"],"description":"Required with `invoiceType` `partial` or `down_payment`, otherwise not\nallowed.\n\n- `positions`: `progress` with the progress to date of the positions\n  of the source, as in \"Abschlag nach Positionen\" in craftivo. Needs a\n  quote or order confirmation as source. The response has\n  `progressBilling`. The older `selections` (quantities since the last\n  partial invoice) is deprecated; send either `progress` or\n  `selections`.\n- `lump_sum`: `percent` or `netCents` of the order total, net after the\n  discount of the source. Needs a quote or order confirmation as source.\n  Optional `performanceNote`.\n- `free_amount`: `netCents` for a project without an order. Needs\n  `source.type` `project`.\n","properties":{"mode":{"type":"string","enum":["positions","lump_sum","free_amount"]},"progress":{"type":"array","minItems":1,"maxItems":500,"description":"Only for `positions`. The progress to date of each position, as\n`quantity` or as `percent` of the quantity in the source; send one\nof the two per position. Each source position at most once. The\ninvoice bills the progress net minus the net amount of all\nfinalized partial and down payment invoices of the order (see\n`progressBilling`). A position billed earlier and missing here\nkeeps its earlier progress; send 0 to correct it to nothing. It\ncan be combined with earlier `lump_sum` partial invoices.\n","items":{"type":"object","additionalProperties":false,"required":["positionId"],"properties":{"positionId":{"type":"string","description":"Id of a position in `positions` of the source document."},"quantity":{"type":"number","minimum":0,"description":"Progress to date as a quantity. Above the quantity in the source is allowed (extra quantity)."},"percent":{"type":"number","minimum":0,"description":"Progress to date in percent of the quantity in the source, rounded to 2 decimals of the quantity as in craftivo. Above 100 is allowed (extra quantity)."}}}},"selections":{"type":"array","minItems":1,"maxItems":500,"deprecated":true,"x-deprecated-at":"2026-11-12","x-sunset":"2027-06-01","description":"Deprecated: send `progress` instead. Only for `positions`. The\nquantities billed since the last partial invoice; the invoice bills\nthem in full, without deducting earlier partial invoices. Each\nsource position at most once.\n","items":{"type":"object","additionalProperties":false,"required":["positionId","quantity"],"properties":{"positionId":{"type":"string","description":"Id of a position in `positions` of the source document."},"quantity":{"type":"number","exclusiveMinimum":0,"description":"May exceed the remaining quantity of the position. Under a\nunit-price contract, extra quantities are billed with the\npartial invoice. If the quantity billed so far then exceeds the\nquantity of the order, the PDF shows both below the position.\n"}}}},"percent":{"type":"number","exclusiveMinimum":0,"description":"Only for `lump_sum`. Send either `percent` or `netCents`. Above 100 is allowed (extra quantities with unit prices)."},"netCents":{"type":"integer","minimum":1,"description":"Net amount in cents, for `lump_sum` or `free_amount`."},"performanceNote":{"type":"string","description":"Only for `lump_sum`. Printed below the invoice line."}}},"UpdateQuoteInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Only the fields sent change; everything else stays as saved, including\ntexts, discounts, payment terms and PDF settings set in craftivo.\n`positions` and `appendPositions` cannot be combined.\n","properties":{"title":{"type":"string","minLength":1,"description":"Cannot be cleared."},"introductionText":{"type":["string","null"],"description":"Empty string or null = no text."},"conclusionText":{"type":["string","null"],"description":"Empty string or null = no text."},"vatOptionId":{"type":"string","description":"Same rules as for `POST /api/v1/quotes`."},"recipientMode":{"type":"string","enum":["customer","manual","property_default"],"description":"Same rules as for `POST /api/v1/quotes`. Switching to `manual`\nneeds `manualRecipient`; a quote that is already `manual` keeps\nits lines when `manualRecipient` is omitted.\n"},"manualRecipient":{"type":"object","additionalProperties":false,"required":["identityLines","addressLines"],"properties":{"identityLines":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string"}},"addressLines":{"type":"array","minItems":1,"maxItems":10,"items":{"type":"string"}},"email":{"type":"string","format":"email"},"phone":{"type":"string"}}},"deliveryEmailOverride":{"type":["string","null"],"description":"Email address that replaces the recipient's. Null removes it. Needs property management enabled."},"positions":{"type":"array","maxItems":500,"description":"Replaces all rows, including section headers, texts and images.\nAn empty array removes them all. Send the sections you read again\n(`type: section`, with their `id` and `parentSectionId`) and the\npositions with their `sectionId`, or the document loses its\nsections. Section settings made in craftivo, such as a lump sum\n(Pauschalpreis), hidden prices or a collapsed section, are lost.\n","items":{"$ref":"#/components/schemas/QuotePositionInput"}},"appendPositions":{"type":"array","maxItems":500,"description":"Adds rows after the existing ones. Existing rows keep all their\ndata, including measurements (Aufmaß), calculation and sections.\nNew rows continue the last section: `sectionId` is the real id of\nthe open section or its main section, or of a new section in this\nrequest. To fill an earlier section, send `positions`. Rows\nwithout section headers and without `sectionId` land as in\ncraftivo, in the last section of a document whose positions have\nno `sectionId` yet; see the position rules.\n","items":{"$ref":"#/components/schemas/QuotePositionInput"}},"version":{"type":"integer","minimum":1,"description":"`version` from the last read. If the quote was saved since, the\nrequest fails with 409 `version_conflict` and nothing changes.\nWithout it the last write wins. Recommended.\n"}}},"UpdateInvoiceInput":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Only the fields sent change; everything else stays as saved, including\ntexts, discounts, payment terms and PDF settings set in craftivo.\n`positions` and `appendPositions` cannot be combined. The fields work as\nin `UpdateQuoteInput`.\n","properties":{"title":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/title"},"introductionText":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/introductionText"},"conclusionText":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/conclusionText"},"vatOptionId":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/vatOptionId"},"recipientMode":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/recipientMode"},"manualRecipient":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/manualRecipient"},"deliveryEmailOverride":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/deliveryEmailOverride"},"positions":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/positions"},"appendPositions":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/appendPositions"},"version":{"$ref":"#/components/schemas/UpdateQuoteInput/properties/version"},"serviceDate":{"type":["string","null"],"format":"date","description":"Service date (§ 14 UStG), a calendar date as `YYYY-MM-DD`. Send either\n`serviceDate` or `servicePeriod`, not both (400 `invalid_body`). It\nreplaces a service date or period the invoice has so far. It may be\nbefore or after the invoice date. `null` removes the service time,\nwhichever of the two it is.\n"},"servicePeriod":{"type":["object","null"],"additionalProperties":false,"required":["from","to"],"description":"Service period (§ 14 UStG), both days as `YYYY-MM-DD`. `from` must not\nbe after `to`; the same day is allowed. Half a period answers 400\n`invalid_body`. Replaces a service date or period the invoice has so far.\n`null` removes the service time, whichever of the two it is.\n","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"}}}}},"QuoteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/QuoteDetail"}}},"CostEstimateResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/CostEstimateDetail"}}},"OrderConfirmationResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/OrderConfirmationDetail"}}},"DeliveryNoteResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/DeliveryNoteDetail"}}},"InvoiceResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/InvoiceDetail"}}},"InvoiceFinalizedResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/InvoiceFinalized"}}},"QuoteListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/QuoteRead"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CostEstimateListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CostEstimateRead"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"OrderConfirmationListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OrderConfirmationRead"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"DeliveryNoteListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/DeliveryNoteRead"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"InvoiceListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InvoiceRead"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"WebhookFields":{"type":"object","required":["id","createdAt","updatedAt","url","events","isActive","source","disabledReason"],"properties":{"id":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"url":{"type":"string","format":"uri","description":"Public https target. Local and private addresses are rejected."},"events":{"type":"array","items":{"type":"string"},"description":"Subscribed events. Wildcards are allowed: `*` for everything,\n`customer.*` / `project.*` / `task.*` for a resource.\n","examples":[["customer.created","project.*"]]},"isActive":{"type":"boolean","description":"Disabled endpoints receive no deliveries. craftivo disables an\nendpoint automatically after 10 final delivery failures in a row or\nimmediately when it answers with HTTP 410.\n"},"source":{"type":"string","enum":["ui","api"],"description":"Where the endpoint was created. This API only ever lists and changes\nendpoints with `source: api`; endpoints created in the settings UI\nare managed there.\n"},"disabledReason":{"type":["string","null"],"enum":["delivery_failures","gone",null]}}},"Webhook":{"allOf":[{"$ref":"#/components/schemas/WebhookFields"}],"unevaluatedProperties":false},"WebhookWithSecret":{"allOf":[{"$ref":"#/components/schemas/WebhookFields"},{"type":"object","required":["secret"],"properties":{"secret":{"type":"string","description":"Signing secret — returned on creation and after a rotation, and\nnever by any other endpoint. Store it now. The only way to see it\nagain is replaying the exact same request with the same API key\nand its original `Idempotency-Key`, which returns the stored response.\n","examples":["whsec_9f2c…"]}}}],"unevaluatedProperties":false},"WebhookResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Webhook"}}},"WebhookWithSecretResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookWithSecret"}}},"WebhookListResponse":{"type":"object","additionalProperties":false,"required":["data","meta"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Webhook"}},"meta":{"$ref":"#/components/schemas/PaginationMeta"}}},"CreateWebhookInput":{"type":"object","additionalProperties":true,"required":["url","events"],"properties":{"url":{"type":"string","format":"uri","examples":["https://hooks.zapier.com/hooks/catch/123/abcdef"]},"events":{"$ref":"#/components/schemas/WebhookSubscriptions"}}},"WebhookSubscriptions":{"type":"array","minItems":1,"description":"Subscribed events. The catalog is closed — an unknown entry is rejected\nwith HTTP 400.\n","items":{"type":"string","enum":["*","customer_request.*","customer_request.created","customer_request.updated","customer_request.converted","customer_request.archived","member.*","member.created","member.updated","member.deactivated","customer.*","customer.created","customer.updated","project.*","project.created","project.updated","project.deleted","property.*","property.created","property.updated","property.deleted","property.restored","task.*","task.created","task.updated","task.completed","form_submission.*","form_submission.created","form_instance.*","form_instance.created","form_instance.submitted","time_entry.*","time_entry.created","time_entry.updated","time_entry.deleted","schedule_entry.*","schedule_entry.created","schedule_entry.updated","schedule_entry.deleted","maintenance_visit.*","maintenance_visit.scheduled","maintenance_visit.completed","maintenance_contract.*","maintenance_contract.created","maintenance_contract.updated","maintenance_contract.archived","maintenance_contract.restored","quote.*","quote.created","quote.updated","quote.finalized","quote.sent","quote.accepted","quote.rejected","cost_estimate.*","cost_estimate.created","cost_estimate.updated","cost_estimate.finalized","cost_estimate.sent","cost_estimate.accepted","cost_estimate.rejected","order_confirmation.*","order_confirmation.created","order_confirmation.updated","order_confirmation.finalized","order_confirmation.sent","order_confirmation.accepted","order_confirmation.rejected","delivery_note.*","delivery_note.created","delivery_note.updated","delivery_note.finalized","delivery_note.sent","delivery_note.accepted","delivery_note.rejected","invoice.*","invoice.created","invoice.updated","invoice.finalized","invoice.sent","invoice.accepted","invoice.rejected","invoice.paid","invoice.partially_paid","invoice.overdue","invoice.cancelled","payment.*","payment.created"]},"examples":[["customer.created","project.*"]]},"UpdateWebhookInput":{"type":"object","additionalProperties":true,"properties":{"url":{"type":"string","format":"uri"},"events":{"$ref":"#/components/schemas/WebhookSubscriptions"},"isActive":{"type":"boolean"}}},"WebhookDeletedResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["id","deleted"],"properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}}}}},"WebhookTestResponse":{"type":"object","additionalProperties":false,"required":["data"],"properties":{"data":{"type":"object","additionalProperties":false,"required":["deliveryId","status"],"properties":{"deliveryId":{"type":"string"},"status":{"type":"string","enum":["pending"]}}}}}}},"paths":{"/api/v1/retentions":{"get":{"operationId":"listRetentions","summary":"List open retentions","description":"Retained and released retentions of the organization. Requires the `financials` permission and full project access (`projects:full`).","tags":["Payments"],"responses":{"200":{"description":"Open retentions.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Retention"}}}}}}}}}},"/api/v1/retentions/{retentionId}/release":{"post":{"operationId":"releaseRetention","summary":"Release a retention","description":"The amount becomes open and can become overdue and be reminded. No early payment discount applies to it. A retention already contained in a final invoice cannot be released.","tags":["Payments"],"parameters":[{"name":"retentionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","enum":["due","guarantee"],"description":"`guarantee`: replaced by a bank guarantee."}}}}}},"responses":{"200":{"description":"The retention after the change.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Retention"}}}}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/api/v1/retentions/{retentionId}/revoke":{"post":{"operationId":"revokeRetentionRelease","summary":"Take back the release of a retention","description":"Only while nothing has been paid on the released amount.","tags":["Payments"],"parameters":[{"name":"retentionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The retention after the change.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Retention"}}}}}},"402":{"$ref":"#/components/responses/PaymentRequired"}}}},"/api/v1/retentions/{retentionId}/forfeit":{"post":{"operationId":"forfeitRetention","summary":"Close a retention the customer keeps for good","description":"For example because of defects. The retention is no longer reminded or open. craftivo books no tax correction; clarify the tax treatment with your tax advisor. Only for retentions of invoices and final invoices without a payment on them. Retentions of partial invoices run through the final invoice.","tags":["Payments"],"parameters":[{"name":"retentionId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","maxLength":500,"description":"Why the customer keeps the retention. Leading and trailing spaces are removed."}}}}}},"responses":{"200":{"description":"The retention after the change.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Retention"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"402":{"$ref":"#/components/responses/PaymentRequired"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/v1/retentions/{retentionId}/forfeit/revoke":{"post":{"operationId":"revokeRetentionForfeit","summary":"Take back closing a kept retention","description":"The retention is retained again.","tags":["Payments"],"parameters":[{"name":"retentionId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The retention after the change.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/Retention"}}}}}},"402":{"$ref":"#/components/responses/PaymentRequired"},"409":{"$ref":"#/components/responses/Conflict"}}}},"/api/v1/me":{"get":{"operationId":"getMe","tags":["Account"],"summary":"Get the authenticated API user and its organizations","description":"Discovery endpoint. Works on every plan and needs no\n`X-Organization-Id` header; it lists the organizations the API key may\nact on, with the role and the permissions of the key's creator in each.\nAn assistant can check `permissions` before its first call instead of\nlearning them from a 403.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/properties":{"get":{"operationId":"listProperties","tags":["Properties"],"summary":"List properties (Objekte), newest first","description":"Properties deleted in craftivo are not listed. With `customerId` or\n`partyId` the whole result is sorted newest first and pages by offset;\na cursor only works with the filters it was returned for.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"},"description":"Only properties where this customer is a party (active or ended)."},{"in":"query","name":"partyId","required":false,"schema":{"type":"string"},"description":"The property of this party."},{"in":"query","name":"query","required":false,"schema":{"type":"string","maxLength":200},"description":"Words in the name, external id or address of the property. Not case-sensitive."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createProperty","tags":["Properties"],"summary":"Create a property (Objekt)","description":"Creates the building only. Attach customers with `POST /api/v1/parties`,\nthen set default parties with `PATCH /api/v1/properties/{propertyId}`.\nLogged in the activity log with the API key as actor; sends\n`property.created`. An address starts the same geocoding as in craftivo.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePropertyInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyDetailResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/properties/{propertyId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"propertyId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getProperty","tags":["Properties"],"summary":"Get a property with its customers, parties and projects","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyDetailResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateProperty","tags":["Properties"],"summary":"Update a property and its default parties","description":"Changes only the fields sent. The four default parties must be active\nparties of this property; null removes one. Logged in the activity log\nwith the API key as actor; sends `property.updated`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePropertyInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyDetailResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteProperty","tags":["Properties"],"summary":"Delete a property (deletion mark, can be restored)","description":"Sets a deletion mark, like the delete button in craftivo. Nothing is\nremoved: Linked projects, documents and parties stay, and finalized\ndocuments keep their recipient. The property leaves all lists; single\nreads answer 410. Saving a draft on it in craftivo no longer uses its\nparties; the recipient falls back to the document's customer, the\naddress to the project or the customer. `PATCH` on such a quote with a\nproperty recipient answers 400.\n`POST /api/v1/properties/{propertyId}/restore` brings it back.\nNeeds `projects:full`, also for a member who may otherwise write\nproperties. Logged in the activity log with the API key as actor; sends\n`property.deleted`. A repeated call answers 410 and writes nothing.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/properties/{propertyId}/restore":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"propertyId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"restoreProperty","tags":["Properties"],"summary":"Restore a deleted property","description":"Removes the deletion mark. Parties and default parties apply as before.\nNeeds `projects:full`. Logged in the activity log with the API key as\nactor; sends `property.restored`. On an active property it answers 200\nand changes nothing. A repeated call runs again and checks permissions\nanew, also with the same `Idempotency-Key`; it never replays an earlier\nresponse.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Restored","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyDetailResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/parties":{"get":{"operationId":"listParties","tags":["Properties"],"summary":"List parties of properties","description":"Active parties of live properties. Ended parties only with\n`includeInactive=true`. Without `propertyId`, `customerId` and `query`\nthe list follows the customer; a page can then hold fewer than `limit`\nparties while `meta.isDone` is false. With one of these filters it is\nsorted newest first and pages by offset. A cursor only works with the\nfilters it was returned for.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"propertyId","required":false,"schema":{"type":"string"},"description":"Parties of this property. A deleted property answers 410."},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},{"in":"query","name":"role","required":false,"schema":{"type":"string","enum":["hausverwaltung","weg","owner","tenant","hausmeister","facility_manager","haustechniker","architekt","energieberater","versicherung","eigentuemervertreter","sonstige"]}},{"in":"query","name":"query","required":false,"schema":{"type":"string","maxLength":200},"description":"Words in the name, external id or address of the property. Only the 100 best-matching properties are searched; narrow the words to reach others."},{"in":"query","name":"includeInactive","required":false,"schema":{"type":"boolean","default":false}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartyListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createParty","tags":["Properties"],"summary":"Attach a customer to a property as party","description":"A property has at most one active party with role `hausverwaltung`\n(409 `conflict`). The customer must not be deleted. Logged in the\nactivity log of the property; sends no webhook.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePartyInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/parties/{partyId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"partyId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getParty","tags":["Properties"],"summary":"Get a party with its customer and default roles","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateParty","tags":["Properties"],"summary":"Update a party","description":"Changes only the fields sent; the property of a party cannot change.\n`isActive: false` ends the party. Logged in the activity log of the\nproperty; sends no webhook.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePartyInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartyResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"archiveParty","tags":["Properties"],"summary":"Archive a party","description":"Ends the party, like `PATCH` with `isActive: false`; the row stays and\n`GET /api/v1/parties?includeInactive=true` still lists it. Needs the\nsame permission as `PATCH`. `PATCH` with `isActive: true` brings it back.\nDefault parties of the property keep pointing at it (`defaultFor`).\nIn craftivo new documents skip such a default and fall back to the next\nrule. `POST /api/v1/quotes` and `PATCH` with a property recipient answer\n400 until `PATCH /api/v1/properties/{propertyId}` removes the default or\n`recipientMode` changes. A draft addressed to this party keeps its\nrecipient, and `PATCH` on it answers 400 until `recipientMode` changes.\nFinalized documents do not change. Logged\nin the activity log of the property with the API key as actor; sends no\nwebhook. A repeated call answers 200 and writes nothing.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Archived","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartyResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts":{"get":{"operationId":"listMaintenanceContracts","tags":["Maintenance"],"summary":"List maintenance contracts with equipment and next due date","description":"Returns the maintenance contracts of the organization, newest first.\nArchived contracts are left out unless `includeArchived=true`; deleted\ncontracts never appear.\nOne request reads at most 300 contracts. With `query` or `status`, or\nwith `customerId` or `propertyId` and archived contracts, a page can\nhold fewer than `limit` contracts, or none, while `meta.isDone` is\n`false`; keep following `meta.cursor`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},{"in":"query","name":"propertyId","required":false,"schema":{"type":"string"},"description":"Property (Objekt). A deleted property returns an empty page."},{"in":"query","name":"status","required":false,"description":"Comma-separated contract statuses.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["aktiv","pausiert","beendet"]}}},{"in":"query","name":"query","required":false,"schema":{"type":"string","maxLength":200},"description":"Words that all must appear in the contract number, the title, the customer name or the type, manufacturer, model or serial number of a device. Not case-sensitive."},{"in":"query","name":"includeArchived","required":false,"schema":{"type":"boolean","default":false},"description":"Also list archived contracts."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createMaintenanceContract","tags":["Maintenance"],"summary":"Create a maintenance contract, optionally from a maintenance type","description":"Creates the contract as the dialog in craftivo does: contract number,\nequipment, first open visit for an active contract. Needs a read-write key\nand the maintenance permission `full`. A customer of another organization\nor a deleted one answers 400 `invalid_customer`, such a property 400\n`invalid_property`, such a maintenance type 400 `invalid_template`. A retry\nwith the same `Idempotency-Key` creates nothing new; its answer needs the\nsame permission as the first request.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMaintenanceContractInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts/{contractId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"contractId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getMaintenanceContract","tags":["Maintenance"],"summary":"Get a maintenance contract with equipment, interval, next due date and property","description":"Archived contracts are returned with `archivedAt`. A deleted contract and a contract of another organization return 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateMaintenanceContract","tags":["Maintenance"],"summary":"Change a maintenance contract","description":"Changes the contract as the dialog in craftivo does. Needs a read-write key\nand the maintenance permission `full`. A deleted contract and a contract of\nanother organization return 404. An archived contract answers 409\n`contract_archived`; restore it first.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMaintenanceContractInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-visits":{"get":{"operationId":"listMaintenanceVisits","tags":["Maintenance"],"summary":"List maintenance visits whose target month lies in a date range","description":"Returns the visits whose `dueDate` (first day of the target month) lies\nin `from`…`to` (at most 366 days), sorted by `dueDate`, then by\ncreation. For \"which maintenance is due in October\" use\n`from=2026-10-01&to=2026-10-31`.\nCompleted visits are always included. Open and scheduled visits only\nappear for active contracts that are not archived, like in the\nmaintenance plan of the app.\nOne request reads at most 300 visits. With `status` or `memberId`, a\npage can hold fewer than `limit` visits, or none, while `meta.isDone`\nis `false`; keep following `meta.cursor`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"First day (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Last day (inclusive), `YYYY-MM-DD`. At most 365 days after `from`."},{"in":"query","name":"contractId","required":false,"schema":{"type":"string"}},{"in":"query","name":"status","required":false,"description":"Comma-separated visit statuses.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["offen","eingeplant","erledigt"]}}},{"in":"query","name":"memberId","required":false,"schema":{"type":"string"},"description":"Member id as returned by `GET /api/v1/members`. Matches visits whose assignment on the planning board has this member, also through the crew."},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-visits/{visitId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"visitId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getMaintenanceVisit","tags":["Maintenance"],"summary":"Get a maintenance visit with result, extras and invoice draft","description":"Adds the technician notes, the protocol, the recorded extras without prices and, with maintenance permission `full`, the invoice drafts. A deleted visit, a visit of a deleted contract and a visit of another organization return 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts/{contractId}/status":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"contractId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"setMaintenanceContractStatus","tags":["Maintenance"],"summary":"Set the status of a maintenance contract","description":"Sets `aktiv`, `pausiert` or `beendet` as in craftivo. Back to `aktiv` creates\nthe next open visit when none is left. Sending the current status changes\nnothing. Needs the maintenance permission `full`. An archived contract\nanswers 409 `contract_archived`; restore it first.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMaintenanceContractStatusInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts/{contractId}/archive":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"contractId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"archiveMaintenanceContract","tags":["Maintenance"],"summary":"Archive a maintenance contract","description":"The contract leaves the list, the search and scheduling, and stays complete.\nOpen and scheduled visits rest with it, and their assignments leave the\nplanning board. Completed and billed visits stay untouched. Archiving it\nagain changes nothing. Needs the maintenance permission `full`. The way back\nis `POST /api/v1/maintenance-contracts/{contractId}/restore`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts/{contractId}/restore":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"contractId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"restoreMaintenanceContract","tags":["Maintenance"],"summary":"Restore a maintenance contract from the archive","description":"The contract returns with its visits as `offen`, without date and people;\nschedule them again. Restoring a contract that is not archived changes\nnothing. Needs the maintenance permission `full`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceContractResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-templates":{"get":{"operationId":"listMaintenanceTemplates","tags":["Maintenance"],"summary":"List the maintenance types and templates of the organization","description":"The ids for `templateId` on a contract, sorted by name. A template holds the\ninterval, the preferred month, the price and the positions for new contracts.\nHolds prices, so it needs the maintenance permission `full`, also with a\nread-only key. Deleted templates do not appear.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceTemplateListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-contracts/{contractId}/schedule-visits":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"contractId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"scheduleMaintenanceVisits","tags":["Maintenance"],"summary":"Schedule the next visit of a maintenance contract","description":"Puts the next open visit of the contract on the planning board with an\norder number and notifies the members, as in the web app. A visit with a\ndate but no people gets them on its existing assignment. Answers 409\n`schedule_conflict` without writing when someone is already booked or\nabsent. Needs the maintenance permission `full`. A retry with the same\n`Idempotency-Key` plans nothing new.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleMaintenanceVisitsInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitScheduleResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`schedule_conflict`: Someone is booked or absent and nothing was\nwritten; `details.conflicts` lists the assignments, `force: true`\nschedules anyway. `schedule_duplicate`: A member is already\nscheduled for this visit's order at an overlapping time; `force:\ntrue` does not override it. `visit_already_scheduled`: The next visit has a date\nand people; `details.scheduleEntryId` names the assignment to change.\n`contract_not_schedulable`: The contract is paused, ended or archived.\n`no_open_visit`: The contract has no visit left to schedule.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryConflictError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/maintenance-visits/{visitId}/complete":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"visitId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"completeMaintenanceVisit","tags":["Maintenance"],"summary":"Complete a maintenance visit with its result","description":"Marks the visit as done, completes its assignment on the planning board\nand creates the next open visit of an active contract, as \"Erledigt\" in\nthe web app. Completing a visit again changes nothing and answers 200\nwith `alreadyCompleted: true`. Needs the maintenance permission `full`.\nPhotos are not part of the API. A deleted visit, a visit of a deleted\ncontract and a visit of another organization return 404.\nAnswers 409 `visit_archived` when the visit or its contract is archived,\nand 409 `technician_note_exists` when the visit already has another note;\nthen complete it without `technicianNote`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompleteMaintenanceVisitInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaintenanceVisitCompleteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/members":{"get":{"operationId":"listMembers","tags":["Members"],"summary":"List active members that can be assigned to tasks","description":"Returns `id`, `createdAt`/`updatedAt` (both mirroring the membership's `joinedAt`), `name`, `role`, and `memberType`. Support sessions, inactive users, permissions, and contact details are excluded.","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MemberListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/positions":{"get":{"operationId":"listPositions","tags":["Positions"],"summary":"List or search the position library","description":"Without `query`, returns entries newest first. With `query`, runs the full-text search over title, description, article number, EAN, and supplier: every word must match, the most used entries come first, and at most 1,000 candidates are considered. This is not a semantic search. An entry whose usage count changes while you page through the results can move to another page, be repeated, or be skipped; run the search again to see the current order. Filtering by `category` can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`. A cursor only works with the same mode (with or without `query`).","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"query","required":false,"description":"Full-text search terms, up to 200 characters.","schema":{"type":"string","maxLength":200}},{"in":"query","name":"category","required":false,"description":"Category, ignoring case and surrounding spaces.","schema":{"type":"string"}},{"in":"query","name":"articleNumber","required":false,"description":"Exact article number. Can match several entries.","schema":{"type":"string"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createPosition","tags":["Positions"],"summary":"Create an entry of the position library","description":"Creates the entry as the dialog in craftivo does. Needs a read-write key\nand the permission `documents`; `unitPriceCents` also needs the permission\nto see sales prices and `costPriceCents` the permission `financials`.\nWithout them the request answers 403 and creates nothing. Calculations,\nwholesaler articles and bulk imports stay in craftivo. Entries cannot be\ndeleted or archived through the API. A retry with the same\n`Idempotency-Key` creates nothing new; its answer needs the same\npermissions as the first request.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePositionInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/positions/{positionId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"positionId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getPosition","tags":["Positions"],"summary":"Get a position of the library","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updatePosition","tags":["Positions"],"summary":"Change an entry of the position library","description":"Changes the entry as the dialog in craftivo does, and linked offer templates\nfollow. Send only what changes. Needs a read-write key and the permission\n`documents`; `unitPriceCents` also needs the permission to see sales prices\nand `costPriceCents` the permission `financials`. An entry of another\norganization returns 404. An entry with a price calculation answers 409\n`position_price_calculated` to `unitPriceCents` and `quantity`; a wholesaler\narticle answers 409 `position_ids_price` to `costPriceCents`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePositionInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PositionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/absences":{"get":{"operationId":"listAbsences","tags":["Absences"],"summary":"List absences of members in a date range","description":"Returns every absence that overlaps `from`…`to` (at most 366 days),\nsorted by start date, then member id. Members that `GET /api/v1/members`\ndoes not list are left out. Without the time tracking permission at\nlevel `view` the list only contains the absences of the API user; a\n`memberId` of another member returns 403. Without the employees\npermission at level `full`, sick leave has the type `absent` and the\nfilter `type=sick` returns 403.\nWithout `memberId`, a range that would read more than 4,000 absences of\nthe organization returns 400 `invalid_filter`; filter by `memberId`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"First day (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Last day (inclusive), `YYYY-MM-DD`. At most 365 days after `from`."},{"in":"query","name":"memberId","required":false,"schema":{"type":"string"},"description":"Member id as returned by `GET /api/v1/members`."},{"in":"query","name":"type","required":false,"description":"Comma-separated types. `absent` matches sick leave of API users who see it as `absent`.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["vacation","sick","personal","unpaid","parental_leave","other","absent"]}}},{"in":"query","name":"status","required":false,"description":"Comma-separated statuses.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["pending","approved","rejected"]}}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown member ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"requestAbsence","tags":["Absences"],"summary":"Request an absence, or enter one for another member","description":"Same rules as in the app. Without `memberId` the absence is requested for the\nkey's creator and waits for a decision (`pending`); everyone who may decide gets\na notification and an email. Entering one for another member needs the\n`time_tracking` full permission, is approved at once and notifies that member.\nA half day and a time window only apply to a single day; a time window only to\n`personal`, `unpaid` and `other`. Otherwise they are dropped, like in the app.\nThe note stays internal: No response contains it. Vacation balances are not\navailable through this API.\n\nWith `Idempotency-Key`, a repeat creates nothing and answers with the current\nabsence and `Idempotency-Replayed: true`. Permission and visibility apply to every\ncall. If that absence was deleted in the meantime, the repeat answers 404.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAbsenceInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`absence_overlap`: Another absence of the member overlaps. `details.conflict`\nnames its type, status and dates, with sick leave hidden like in\n`GET /api/v1/absences`, never its note. Send `allowOverlap: true` to save anyway.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/absences/{absenceId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"absenceId","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"patchAbsence","tags":["Absences"],"summary":"Change an absence by the same rule as the app","description":"Whoever may change the absence in the app may change it here, in the same states:\nthe member while it is `pending`, members with the `time_tracking` full permission\nalways. Send only what changes. Dates, half day and time window behave as in\n`POST /api/v1/absences`; send any of `isHalfDay`, `startTime` and `endTime` and all\nthree are replaced together. An empty `notes` removes the note. Changing does not\nchange the status and sends no notification, like in the app.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAbsenceInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`absence_not_pending`: The absence is decided and the key's creator lacks\nthe `time_tracking` full permission. `absence_overlap`: Another absence of the\nmember overlaps; `details.conflict` names its type, status and dates, with\nsick leave hidden like in `GET /api/v1/absences`, never its note.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteAbsence","tags":["Absences"],"summary":"Delete an absence by the same rule as the app","description":"Whoever may delete the absence in the app may delete it here, in the same states:\nthe member while it is `pending`, members with the `time_tracking` full permission\nalways, which also returns approved days to the vacation balance. The absence is\nremoved for good; the audit log keeps the deletion with the key as actor. A repeated\ncall answers 404.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`absence_not_pending`: The absence is decided and the key's creator lacks\nthe `time_tracking` full permission.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/absences/{absenceId}/approve":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"absenceId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"approveAbsence","tags":["Absences"],"summary":"Approve an absence","description":"Needs the `time_tracking` full permission. The member gets a notification and an\nemail. Approving an approved absence changes nothing and sends nothing. A rejected\nabsence can be approved, like in the app. With `Idempotency-Key`, a repeat changes\nnothing and answers with the current absence and `Idempotency-Replayed: true`;\nthe permission applies to every call.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/absences/{absenceId}/reject":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"absenceId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"rejectAbsence","tags":["Absences"],"summary":"Reject an absence","description":"Needs the `time_tracking` full permission. The member gets a notification and an\nemail with the reason. Rejecting a rejected absence changes nothing and sends\nnothing, also with another reason. An approved absence can be rejected, like in the\napp. With `Idempotency-Key`, a repeat changes nothing and answers with the current\nabsence and `Idempotency-Replayed: true`; the permission applies to every call.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RejectAbsenceInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AbsenceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/company-vacations":{"get":{"operationId":"listCompanyVacations","tags":["Absences"],"summary":"List company vacations in a date range","description":"Returns every company vacation (Betriebsferien) that overlaps\n`from`…`to` (at most 366 days), sorted by start date. Every member may\nread them. The absences of the members that belong to a company\nvacation are part of `GET /api/v1/absences` with `companyVacationId`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"First day (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Last day (inclusive), `YYYY-MM-DD`. At most 365 days after `from`."}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompanyVacationListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/holidays":{"get":{"operationId":"listHolidays","tags":["Absences"],"summary":"List public holidays of the organization in a date range","description":"Returns the holidays of the organization between `from` and `to` (at\nmost 366 days), sorted by date. The list holds the holidays imported\nfrom the holiday region of the organization and the ones added by\nhand. A recurring holiday returns one entry for every year in the\nrange. Every member may read them.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"First day (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Last day (inclusive), `YYYY-MM-DD`. At most 365 days after `from`."}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HolidayListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/sandbox/reset":{"get":{"operationId":"getSandboxReset","tags":["Sandbox"],"summary":"Read the state of the latest sandbox reset","description":"Poll this route after `POST /api/v1/sandbox/reset` until `status` is\n`completed` (every two seconds is enough; a reset usually takes a few\nseconds). Answers `403 not_a_sandbox` outside a practice account.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxResetResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/SandboxForbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"startSandboxReset","tags":["Sandbox"],"summary":"Reset the practice account to its sample data","description":"Deletes all data of the practice account for good (no trash, no\nretention) and creates the sample data again, with fresh document\nnumbers. Members, API keys with their quota, and webhook endpoints stay.\nThe webhook endpoints receive the `created` events of the new sample\ndata. The run continues in the background: the answer is\n`202 Accepted` with `status: running`; poll `GET /api/v1/sandbox/reset`\nuntil `status` is `completed`. Only a read-write key whose creator is\nthe owner of the practice account may start a reset. Any other organization\nanswers `403 not_a_sandbox` and keeps all its data. A retry with the\nsame `Idempotency-Key` answers with the run it started, also after that\nrun deleted the data, and starts no second reset.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"202":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Accepted, the reset runs.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SandboxResetResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/SandboxForbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/SandboxResetConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/schedule-entries":{"get":{"operationId":"listScheduleEntries","tags":["Schedule"],"summary":"List assignments of the planning board in a date range","description":"Returns every assignment that overlaps `from`…`to` (at most 56 days),\nsorted by start date, then start time on that day (all-day\nassignments after timed ones), then id. A recurring assignment returns\none entry per date in the range, each with `recurrenceId`; the series\nitself is not listed. Members without the planning permission only get\ntheir own released assignments in the window of their app (see the tag\ndescription); the rest of the range stays empty for them.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"from","required":true,"schema":{"type":"string","format":"date"},"description":"First day (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":true,"schema":{"type":"string","format":"date"},"description":"Last day (inclusive), `YYYY-MM-DD`. At most 55 days after `from`."},{"in":"query","name":"memberId","required":false,"schema":{"type":"string"},"description":"Member id as returned by `GET /api/v1/members`; also matches through the crew."},{"in":"query","name":"teamId","required":false,"schema":{"type":"string"},"description":"Crew id as returned by `GET /api/v1/teams`."},{"in":"query","name":"projectId","required":false,"schema":{"type":"string"}},{"in":"query","name":"status","required":false,"description":"Comma-separated statuses. For dates of a recurring assignment the status of the date counts.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["draft","tentative","planned","confirmed","scheduled","in_progress","paused","completed","cancelled"]}}},{"in":"query","name":"entryType","required":false,"description":"Comma-separated entry types. Pre-planning bars have entry type `project` and status `draft`. Older assignments without entry type never match.","style":"form","explode":false,"schema":{"type":"array","items":{"type":"string","enum":["project","workBlock","maintenance"]}}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches, also unknown ids, return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createScheduleEntry","tags":["Schedule"],"summary":"Plan members or a team on a project for one or several days","description":"Creates an assignment as in the planning board and notifies the members.\nAnswers 409 `schedule_conflict` without writing when someone is already\nbooked or absent. A retry with the same `Idempotency-Key` creates nothing\nnew. The answer is `400 invalid_body` for a completed project or a\nproject \"for the office only\" with members who may not see it, and\n`400 invalid_assignee` for a member who is not in `GET /api/v1/members`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScheduleEntryInput"}}}},"responses":{"201":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryWriteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`schedule_conflict`: Someone is booked or absent in this period and\nnothing was written. `details.conflicts` lists the assignments; send\n`force: true` to plan anyway. `schedule_duplicate`: A member is\nalready scheduled for the same project at an overlapping time;\nnothing was written and `force: true` does not override it.\n`use_web_planner`: This assignment changes in the planning board.\n`invalid_status_transition`: The status cannot change this way.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryConflictError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/schedule-entries/{entryId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"entryId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getScheduleEntry","tags":["Schedule"],"summary":"Get an assignment with members, crew, project, notes and status","description":"A recurring assignment returns with its `recurrenceRule` and its whole period. Without the `schedule` permission the key owner sees what the app shows: `startDate` and `endDate` span the first to the last visible date inside the app window, and an `UNTIL` in the rule ends there too. The id of a single date, an assignment of another organization and an assignment the member may not see return 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"patchScheduleEntry","tags":["Schedule"],"summary":"Move an assignment, change its members, notes or status","description":"Same rules as in the planning board, with its notifications to the\nmembers. The id of a recurring assignment, of one of its dates, of\npre-planning and of a project bar answers 409 `use_web_planner`. So does\na new period or time for an assignment with single selected dates. Without the planning\npermission the answer is 403, also for an own assignment.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateScheduleEntryInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryWriteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`schedule_conflict`: Someone is booked or absent in this period and\nnothing was written. `details.conflicts` lists the assignments; send\n`force: true` to plan anyway. `schedule_duplicate`: A member is\nalready scheduled for the same project at an overlapping time;\nnothing was written and `force: true` does not override it.\n`use_web_planner`: This assignment changes in the planning board.\n`invalid_status_transition`: The status cannot change this way.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryConflictError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteScheduleEntry","tags":["Schedule"],"summary":"Delete an assignment or a single date of a recurring assignment","description":"Deletes one assignment with its cascades and notifies the members. With\nthe id of a date (`id` of a list row with `recurrenceId`) only that date\nis cancelled; the series stays. A whole recurring assignment, pre-planning\nand project bars answer 409 `use_web_planner`. A repeated call answers 404.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`schedule_conflict`: Someone is booked or absent in this period and\nnothing was written. `details.conflicts` lists the assignments; send\n`force: true` to plan anyway. `use_web_planner`: This assignment\nchanges in the planning board. `invalid_status_transition`: The\nstatus cannot change this way.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleEntryConflictError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/tasks":{"get":{"operationId":"listTasks","tags":["Tasks"],"summary":"List tasks and activities, newest first","description":"The `type` filter defaults to `task`. Choose another type to list calls, appointments, measurements, emails, or reminders. The list shows the same tasks as `GET /api/v1/tasks/{taskId}`. A page can then hold fewer than `limit` tasks, or none, while `meta.isDone` is `false`. Follow `meta.cursor`. A `projectId` filter for a project that `GET /api/v1/projects/{projectId}` does not show to this key gives an empty list, as for an unknown project.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"type","required":false,"schema":{"type":"string","enum":["task","call","appointment","measurement","email","reminder"],"default":"task"}},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["open","completed"]}},{"in":"query","name":"assigneeId","required":false,"schema":{"type":"string"}},{"in":"query","name":"projectId","required":false,"schema":{"type":"string"}},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},{"in":"query","name":"dueBefore","required":false,"schema":{"type":"string"},"description":"ISO 8601 or unix timestamp in milliseconds."},{"in":"query","name":"dueAfter","required":false,"schema":{"type":"string"},"description":"ISO 8601 or unix timestamp in milliseconds."},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createTask","tags":["Tasks"],"summary":"Create a task or another activity type","description":"Only `title` is required. `type` defaults to `task`, `dueAt` to the end of today in Europe/Berlin, and `assigneeId` to the member configured on the write API key. `projectId` and `projectExternalId` accept only projects that `GET /api/v1/projects/{projectId}` shows to this key; any other project answers 404 `not_found`, as for an unknown project.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/tasks/{taskId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"taskId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getTask","tags":["Tasks"],"summary":"Get a task or activity","description":"`attachments` lists the files of the task with their download links. The key's creator must be allowed to open the task in craftivo. For a task on a project, they must also be allowed to read the project with `GET /api/v1/projects/{projectId}`: without the `projects:full` permission, only with a team row and never for a project marked office only. Any other task answers 404 `not_found`, as for a deleted task.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateTask","tags":["Tasks"],"summary":"Update a task or mark it open or completed","description":"Setting `status` changes the existing `completedAt` timestamp. Changes only tasks that `GET /api/v1/tasks/{taskId}` shows to this key; any other task answers 404 `not_found` and stays unchanged. `projectId` and `projectExternalId` accept only projects that `GET /api/v1/projects/{projectId}` shows to this key; any other project answers 404 `not_found`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTaskInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteTask","tags":["Tasks"],"summary":"Delete a task","description":"Removes the task like the delete button in craftivo. Comments and attachments stay stored, and the task can still be restored in craftivo. Requires the permission for this task type. Deletes only tasks that `GET /api/v1/tasks/{taskId}` shows to this key; any other task answers 404 `not_found` and stays. Only the creator of the task or a member with `projects:full` may delete it; otherwise the answer is 403 `forbidden`. A repeated call answers 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskDeletedResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/tasks/{taskId}/comments":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"taskId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listTaskComments","tags":["Tasks"],"summary":"List the comments of a task, oldest first","description":"A key that `GET /api/v1/tasks/{taskId}` does not show this task to gets 404, as for a deleted task. System events of the task history are not part of this list.","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. A task without comments returns an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskCommentListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"addTaskComment","tags":["Tasks"],"summary":"Add a comment to a task","description":"Writes a comment to the task history under the name of the API key and notifies the people involved, as a comment in craftivo does. Requires that `GET /api/v1/tasks/{taskId}` shows this task to the key; otherwise 404. With `Idempotency-Key` a repeated call first checks access, then answers with the existing comment.","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTaskCommentInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskCommentResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/tasks/{taskId}/files":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"taskId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"attachTaskFile","tags":["Tasks"],"summary":"Attach an uploaded file to a task","description":"References a file id from `POST /api/v1/files` and shows the file on the\ntask in craftivo. The file id is consumed by this call. An invalid,\nforeign, or already used id returns 400 `invalid_file`.\n\nThe limits of craftivo apply: at most 10 attachments per task (409\n`attachment_limit_reached`) and at most 10 MB per document (413\n`file_too_large`). In both cases the file id stays usable. Requires that\n`GET /api/v1/tasks/{taskId}` shows this task to the key; otherwise 404. With\n`Idempotency-Key` a repeated call first checks access, then answers with\nthe existing attachment.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachTaskFileInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TaskAttachmentResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/teams":{"get":{"operationId":"listTeams","tags":["Teams"],"summary":"List active crews with their members","description":"Returns all active crews of the organization, sorted by name, without pagination. Every member of the organization may read them.","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/teams/{teamId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"teamId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getTeam","tags":["Teams"],"summary":"Get a crew with its members","description":"Also returns a deactivated crew (`isActive: false`), for example one that older schedule entries still reference. A crew of another organization returns 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/time-entry-categories":{"get":{"operationId":"listTimeEntryCategories","tags":["Time entries"],"summary":"List active time entry categories","description":"Returns the organization's active categories. Exactly one carries `isDefault: true`; it is used when `POST /api/v1/time-entries` omits `categoryId`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryCategoryListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/time-entries":{"get":{"operationId":"listTimeEntries","tags":["Time entries"],"summary":"List time entries, newest first","description":"Deleted entries are never returned. Members without the `time_tracking` view permission only see their own entries. A key whose creator has the `time_tracking` level `none` answers 403 `forbidden` with `time_tracking:own`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"query","name":"userId","required":false,"schema":{"type":"string"},"description":"Member id as returned by `GET /api/v1/members`."},{"in":"query","name":"projectId","required":false,"schema":{"type":"string"}},{"in":"query","name":"from","required":false,"schema":{"type":"string","format":"date"},"description":"First entry date (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"to","required":false,"schema":{"type":"string","format":"date"},"description":"Last entry date (inclusive), `YYYY-MM-DD`."},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["draft","submitted","approved","rejected"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createTimeEntry","tags":["Time entries"],"summary":"Create a completed time entry for a member","description":"Books `startTime`–`endTime` for `userId` on either `projectId` or\n`orderId` (an order from `GET /api/v1/order-confirmations`; it must be\nlinked to a project). `categoryId` defaults to the organization's\ndefault category. The entry is created in `draft` status and appears\nin the app's time tracking like a manual entry.\n\nThe same rule as in the app applies: without the `projects:full`\npermission, the key's creator must be on the project's team or\nscheduled on it in the planning board, and the project must not be\noffice-only. Any other project answers 404 `not_found` like an unknown\n`projectId` or `orderId`, before its status is checked. A repeated\nrequest with the same `Idempotency-Key` follows the same rule.\n\nIf the key's creator has the `time_tracking` level `none`, the request\nanswers 403 `forbidden` with `time_tracking:own`, also for their own\ntime and before any other check.\n\nA `startTime` more than 5 minutes or an `endTime` more than 2 hours\nafter the server time answers 400 `invalid_body`, unless the\norganization allows time entries in the future.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTimeEntryInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/time-entries/{entryId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"entryId","required":true,"schema":{"type":"string"}}],"patch":{"operationId":"patchTimeEntry","tags":["Time entries"],"summary":"Change times, break, category, project or note of a time entry","description":"Same rules as in the app. Without the `time_tracking` full permission\nthe key's creator changes only their own entries (a crew lead also the\ncrew's) and gets 403 for others, also without any `time_tracking`\npermission. A new `projectId` or `orderId` needs the same access as\nPOST /api/v1/time-entries: on the project's team, scheduled on it, or\nthe full projects permission. Otherwise the answer is 404 `not_found`\nlike for an unknown id, also for a completed project. An entry whose\nproject stays the same can still be corrected without that access.\nA rejected entry becomes a draft again\nafter the change. Every change lands in the entry's history with the\nkey as actor and sends `time_entry.updated`. A request without a real\nchange answers 200 and writes neither. A new `startTime` more than\n5 minutes or a new `endTime` more than 2 hours after the server time\nanswers 400 `invalid_body`, unless the organization allows time\nentries in the future.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimeEntryInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`time_entry_locked`: An invoice or a closed period locks this entry,\nalso for the owner; `details.reason` is `billed` or `period`.\n`time_entry_submitted`: The entry was submitted or approved and the\nkey's creator is not the owner. `time_entry_running`: The timer still\nruns. `conflict`: The entry overlaps another one; send\n`allowOverlap: true` to save anyway.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteTimeEntry","tags":["Time entries"],"summary":"Delete a draft time entry","description":"Deletes only drafts, like the app: The payroll export takes approved\nentries, and an invoice or a closed period locks an entry. Other members'\nentries need the `time_tracking` full permission; a crew lead deletes\nonly their own. The entry's history keeps the deletion with the key as\nactor. Sends `time_entry.deleted`. A repeated call answers 404. craftivo\ndoes not record single exports to PDF, Excel or CSV, so a draft that\nappeared in one can still be deleted.\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"description":"`time_entry_locked`: An invoice or a closed period locks this entry,\nalso for the owner; `details.reason` is `billed` or `period`.\n`time_entry_not_draft`: Only drafts can be deleted; `details.status`\nnames the status. `time_entry_running`: The timer still runs.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customers":{"get":{"operationId":"listCustomers","tags":["Customers"],"summary":"List customers, newest first","description":"Customers deleted in craftivo are not listed.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"in":"query","name":"email","required":false,"description":"Exact match on the primary email address.","schema":{"type":"string"}},{"$ref":"#/components/parameters/ExternalIdFilter"},{"in":"query","name":"customField","required":false,"style":"deepObject","explode":true,"description":"Filter by custom field values — `customField[slug]=value`.","schema":{"type":"object","additionalProperties":{"type":"string"}}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createCustomer","tags":["Customers"],"summary":"Create a customer","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customers/{customerId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"customerId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getCustomer","tags":["Customers"],"summary":"Get a customer","description":"A customer deleted in craftivo returns HTTP 404.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateCustomer","tags":["Customers"],"summary":"Update a customer","description":"A customer deleted in craftivo returns HTTP 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customers/external/{externalId}":{"put":{"operationId":"upsertCustomerByExternalId","tags":["Customers"],"summary":"Create or update a customer by external ID","description":"Upsert for connected systems: an existing customer with this\n`externalId` is updated (HTTP 200), otherwise a new one is created\n(HTTP 201). `firstName` and `lastName` are required for creation.\nIf the customer with this `externalId` was deleted in craftivo, the\nrequest fails with HTTP 409 and is neither restored nor duplicated.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"externalId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerInput"}}}},"responses":{"200":{"description":"Updated","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}},"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-request-columns":{"get":{"operationId":"listCustomerRequestColumns","tags":["Customer requests"],"summary":"List the columns new customer requests can be placed in","description":"Columns of the board on which new requests land (the default customer\nrequest board, otherwise the first one), sorted by `order`. These are the\nids `columnId` accepts on POST and PATCH /customer-requests. `kind` is\n`won` or `lost` for the result columns, otherwise `open`. An\norganization without a board gets an empty list; this call does not\ncreate one.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestColumnListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-requests":{"get":{"operationId":"listCustomerRequests","tags":["Customer requests"],"summary":"List customer requests, newest first","description":"Archived requests are excluded by default. Pass `archived=true` to\ninclude them. `estimatedValue` is omitted without financials permission.\nArchived requests and filters can make a page shorter than `limit`, or\nempty, while `meta.isDone` is `false`; keep following `meta.cursor`.\n\nThe same rule as in the app applies: with the `projects:full`\npermission the key sees every request. Otherwise it sees a request on\na project the key's creator works on, or one the office assigned a\ntask or appointment on to the key's creator; without the `projects`\npermission it sees none. In addition, a linked or converted project\nthat is still active must be one that\n`GET /api/v1/projects/{projectId}` shows to this key: on the project's\nteam and not office-only. Being scheduled on it is not enough. Other\nrequests are left out without losing a row while paging.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},{"in":"query","name":"columnId","required":false,"schema":{"type":"string"}},{"in":"query","name":"archived","required":false,"description":"When true, include archived customer requests.","schema":{"type":"boolean"}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createCustomerRequest","tags":["Customer requests"],"summary":"Create a customer request","description":"Creates the request and places a Kanban card on the organization's\ncustomer request board. Every call requires the `projects:full`\npermission, as in the craftivo app. Without it the request is rejected\nwith 403 `forbidden` and `details.requiredPermission` is\n`projects:full`; a repeated call with the same `Idempotency-Key` gets\nthe same 403, not the stored response. `estimatedValue` also requires\nfinancials permission.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequestInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-requests/{customerRequestId}":{"get":{"operationId":"getCustomerRequest","tags":["Customer requests"],"summary":"Get a customer request","description":"Follows the rule of `GET /api/v1/customer-requests`. A request the key cannot see answers 404 `not_found`, like an unknown id.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"patchCustomerRequest","tags":["Customer requests"],"summary":"Update a customer request","description":"Partial update. Every call requires the `projects:full` permission,\nfor all fields, as in the craftivo app. Without it the request is\nrejected with 403 `forbidden` and `details.requiredPermission` is\n`projects:full`. Reading a customer request does not need this level.\nA request the key cannot see under the rule of\n`GET /api/v1/customer-requests` answers 404 `not_found` first.\n`customerId: null` clears the linked customer.\n`estimatedValue: null` clears the estimated value and requires\nfinancials permission. `columnId` moves the Kanban card to the top of\nthat column. Moving the card to a `won` column does not create a\nproject and moving it to a `lost` column sets no loss reason; convert\nthe request for that. `nextStep`, `followUpDueAt`, `temperature` and\n`assignedUserId`: null clears them.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchCustomerRequestInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteCustomerRequest","tags":["Customer requests"],"summary":"Archive a customer request","description":"Soft delete. Requires the `projects:full` permission, as in the\ncraftivo app; without it the request is rejected with 403 `forbidden`\nand `details.requiredPermission` is `projects:full`. Sets `archivedAt`\nand removes the request from list responses unless `archived=true` is\npassed. The request stays readable via GET, and repeating the call\nkeeps the original `archivedAt` (idempotent). There is no hard delete.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Archived","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-requests/{customerRequestId}/offers":{"post":{"operationId":"createCustomerRequestOffer","tags":["Customer requests"],"summary":"Create a draft quote for a customer request","description":"Always inserts a new draft quote. Callers never send a project id.\nIf the request already has a project or order that is not deleted,\nthe quote belongs to it. Otherwise the quote belongs only to the\ncustomer request and has `projectId: null`, like a quote created in\ncraftivo: no project is created and a deleted one is not restored.\nConverting the request later (`POST .../convert`) creates the project\nand moves the quote to it. Repeat without an\nIdempotency-Key creates another draft. Needs the `create_offer`\npermission like `POST /api/v1/quotes` and, for a request with a\nproject, access to it (see the Documents tag; otherwise 404). A customer without an address\nis rejected with 400, because the quote needs a recipient address.\nA request the key cannot see under the rule of\n`GET /api/v1/customer-requests` answers 404 `not_found` and creates\nneither a quote nor a project.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCustomerRequestOfferInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestQuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-requests/{customerRequestId}/convert":{"post":{"operationId":"convertCustomerRequest","tags":["Customer requests"],"summary":"Convert a customer request into a project or order","description":"Marks the customer request as won, like the conversion dialog in the\ncraftivo UI. Name, description, address, customer and property come\nfrom the request; only the target type can be chosen.\n\n- Without a linked project, a new project or order is created with\n  the next number of its number range (`201`). This fires\n  `project.created`.\n- A draft project of the request is taken over (`200`); no\n  second project is created. `project.created` already fired when\n  the draft was created and does not fire again; `project.updated`\n  fires with the new state (a project now has `visibleOnBoard: true`).\n  With `target: order` it becomes an order and a new order number\n  replaces the project number.\n- An already converted request returns `409 already_converted` with\n  `details.convertedProjectId` and changes nothing.\n\nFiles, forms, tasks, checklists, notes and quotes of the request move\nto the project. `customer_request.converted` fires once; its payload\nis the customer request with `convertedProjectId`. Needs the `projects`\npermission with full scope; a new project additionally needs\n`create_project`. With an Idempotency-Key the status (`201` or `200`)\nand body are replayed.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConvertCustomerRequestInput"}}}},"responses":{"200":{"description":"Existing draft project taken over","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"201":{"description":"New project or order created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/customer-requests/{customerRequestId}/files":{"post":{"operationId":"attachCustomerRequestFile","tags":["Customer requests"],"summary":"Attach an uploaded file to a customer request","description":"References a file id from `POST /api/v1/files` and stores the file in\nthe customer request's file storage — the same place uploads from the\ncraftivo UI go. The file id is consumed by this call and cannot be used\nagain; an invalid, foreign, or already used id returns `invalid_file`.\nA request the key cannot see under the rule of\n`GET /api/v1/customer-requests` answers 404 `not_found` before the file\nid is checked. A repeated request with the same `Idempotency-Key`\nfollows the same rule.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"customerRequestId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachCustomerRequestFileInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerRequestFileAttachmentResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/project-statuses":{"get":{"operationId":"listProjectStatuses","tags":["Projects"],"summary":"List the project statuses of the organization","description":"The statuses `statusId` accepts for projects (`type: project`), sorted by\n`order`. Orders (`type: order`) use a separate status list that this\nendpoint does not return. Requires the projects permission.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectStatusListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/projects":{"get":{"operationId":"listProjects","tags":["Projects"],"summary":"List projects and orders, newest first","description":"Without the `projects:full` permission, the list contains only projects where the key's creator is on the team. Access permissions and filters can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"in":"query","name":"visibleOnBoard","required":false,"description":"Filter by project board eligibility, including archived projects. Omit to retain the full list of projects and orders. Other filters and access permissions still apply. Pages may contain fewer than limit items, including none; follow the cursor until isDone is true.","schema":{"type":"boolean"}},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["draft","planning","in_progress","on_hold","completed","cancelled"]}},{"in":"query","name":"customerId","required":false,"schema":{"type":"string"}},{"$ref":"#/components/parameters/ExternalIdFilter"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Filters without matches return an empty `data` array.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createProject","tags":["Projects"],"summary":"Create a project or order","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/projects/{projectId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"projectId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getProject","tags":["Projects"],"summary":"Get a project or order","description":"Follows the same rule as `GET /api/v1/projects`. Without the `projects:full` permission, the key's creator must be on the project's team, and the project must not be office-only. Being scheduled on the project is not enough. Any other project answers 404 `not_found`, so the answer does not show whether the project exists.","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateProject","tags":["Projects"],"summary":"Update a project or order","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/projects/external/{externalId}":{"put":{"operationId":"upsertProjectByExternalId","tags":["Projects"],"summary":"Create or update a project by external ID","description":"Upsert for connected systems: an existing project with this\n`externalId` is updated (HTTP 200), otherwise a new one is created\n(HTTP 201). `name` is required for creation.\n\nIf the `externalId` belongs to a project that\n`GET /api/v1/projects/{projectId}` does not show to this key, or to a\ndeleted project, the answer is 404 `not_found`. Nothing is changed or\ncreated.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"externalId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectInput"}}}},"responses":{"200":{"description":"Updated","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"410":{"$ref":"#/components/responses/PropertyDeleted"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/projects/{projectId}/members":{"post":{"operationId":"addProjectMember","tags":["Projects"],"summary":"Add a member to a project","description":"Without the `projects:full` permission, the key's creator must be on the project's team, and the project must not be office-only. Being scheduled on the project is not enough. Any other project answers 404 `not_found`, so the answer does not show whether the project exists. A team member who does not lead the project gets 403 `forbidden`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"projectId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMemberInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMemberResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes":{"get":{"operationId":"listQuotes","tags":["Documents"],"summary":"List quotes, newest first","description":"Legacy quotes uploaded as PDF from a previous program (\"Altangebote\") are not documents of this organization and are never listed. Only documents the key may read are listed (see the Documents tag). Filters and this rule can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/DocumentStatus"},{"$ref":"#/components/parameters/DocumentCustomerId"},{"$ref":"#/components/parameters/DocumentProjectId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/DocumentDateFrom"},{"$ref":"#/components/parameters/DocumentDateTo"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createQuote","tags":["Documents"],"summary":"Create a draft quote in a project","description":"Creates a draft quote without a number. Totals, recipient and\nhistory are the same as for a quote created in craftivo; the history\nnames the API key. The key's creator needs the `create_offer`\npermission and access to the project. No webhook fires for a draft:\n`quote.created` follows when the quote is finalized. Repeat without\nan Idempotency-Key creates another draft. Unknown fields, for example\n`deliveryPartyId`, are rejected with 400.\n\nErrors: unknown, deleted or inaccessible project → 404; customer not\nmatching the project → 400 `invalid_customer`; unknown position →\n400; an `articleNumber` that matches more than one position → 409\n`ambiguous_article_number`; `positions` with at least one priced row\n(library or free position) without the `financials` permission of the\nkey's creator → 403 `forbidden`, like `PATCH /api/v1/quotes/{quoteId}`.\nA draft without positions or with section headers only needs no\n`financials` permission, as in craftivo.\n\nThe response shows amounts like craftivo does for the key's creator:\nwithout the `financials` or `salesPrices` permission, the totals\n(`netAmountCents`, `taxAmountCents`, `grossAmountCents`) and the\nposition prices (`unitPriceCents`, `netAmountCents`) are `null`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateQuoteInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes/{quoteId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getQuote","tags":["Documents"],"summary":"Get a quote including its positions","description":"Returns 404 for a legacy quote uploaded as PDF (\"Altangebot\") and for a quote the key may not read (see the Documents tag).","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateQuote","tags":["Documents"],"summary":"Change a draft quote","description":"Changes the header, replaces the positions (`positions`) or adds\npositions (`appendPositions`). Fields and checks for positions and\nrecipient are the same as for `POST /api/v1/quotes`. Totals and\nhistory are the same as for a change in craftivo; the history names\nthe API key. No webhook fires for a draft.\n\nPosition ids are not stable: every change saves all positions anew,\nso ids read before a PATCH are invalid afterwards. Read them again\nfrom the response.\n\nErrors: unknown, deleted or inaccessible quote, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; quote\nno longer a draft (also in customer review) → 409\n`document_finalized`; someone else is editing it in craftivo → 409\n`document_locked`; `version` older than the stored one → 409\n`version_conflict`; read-only key → 403 `insufficient_scope`;\n`positions`, or `appendPositions` with a priced row, without the\n`financials` permission of the key's creator → 403 `forbidden`\n(appending section headers only needs no `financials`, as in\ncraftivo); a quote addressed to the\nproperty (`property_default`) whose property or default party is gone\n→ 400 until `recipientMode` is changed; the same for a quote addressed\nto a party chosen in craftivo (`property_party`) whose property was\ndeleted or whose party was ended or moved to another property. A\ndelivery party chosen on the quote (`deliveryPartyId`) counts instead\nof the property's default delivery party. With a delivery email set on\nthe quote (`deliveryEmailOverride`), no delivery party is checked: an\nended delivery party on the quote or the property does not block the\nchange. Removing the override checks the delivery party again.\n\nThe response shows amounts like craftivo does for the key's creator:\nwithout the `financials` or `salesPrices` permission, the totals\n(`netAmountCents`, `taxAmountCents`, `grossAmountCents`) and the\nposition prices (`unitPriceCents`, `netAmountCents`) are `null`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateQuoteInput"}}}},"responses":{"200":{"description":"Updated","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes/{quoteId}/finalize":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"finalizeQuote","tags":["Documents"],"summary":"Finalize a draft quote","description":"Assigns the next number from the organization's number sequence and\nlocks the quote, as \"Festschreiben\" in craftivo. Afterwards it can\nonly be corrected or cancelled in craftivo; `PATCH` answers 409.\n\nRequires the extra right `documents:finalize` of the API key\n(otherwise 403 `insufficient_scope`, `details.requiredScope`) and the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). No request body.\n\nThe response is the quote in the detail format with `number`.\n`pdfUrl` is `null`: craftivo creates the archived PDF in the\nbackground right after finalizing. Read the document again to get\n`pdfUrl` once it exists. Webhooks fire as\nfor finalizing in craftivo: `quote.created` and\n`quote.finalized`. The history names the API key.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same number. Without a key a retry answers 409\n`document_finalized`.\n\nErrors: unknown, deleted or inaccessible document, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; not a\ndraft → 409 `document_finalized`; someone else is editing it in\ncraftivo → 409 `document_locked`; missing information → 422\n`finalization_incomplete` with `details.missingFields`; too many\nparallel finalizations in the organization → 429 with `Retry-After`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/FinalizationIncomplete"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes/{quoteId}/send":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"sendQuote","tags":["Documents"],"summary":"Email a finalized quote to the customer","description":"Sends the archived PDF of the quote (with XRechnung also the XML\nfile) from the organization's sender mailbox, as \"Per E-Mail\nversenden\" in craftivo.\n\nRequires the extra right `documents:send` of the API key (otherwise\n403 `insufficient_scope`) and the `mailbox` and `financials`\npermissions of the key's creator (otherwise 403 `forbidden`).\n\nRecipient: the delivery address frozen when the document was\nfinalized, e.g. the property management of the building. craftivo\nnever falls back to the customer's email. `to`, `cc` and `bcc`\nreplace it.\n\nUse `dryRun: true` to see the email first. Send an\n`Idempotency-Key`: a retry returns the first response and never sends\na second email. A document that was already sent needs\n`resend: true`. The webhook `quote.sent` fires only when the\nstatus changes from finalized to sent, not for a resend.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendRequest"}}}},"responses":{"200":{"description":"Sent, or the preview for `dryRun`","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SendConflict"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/SendFailed"}}}},"/api/v1/quotes/{quoteId}/accept":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"acceptQuote","tags":["Documents"],"summary":"Mark a sent quote as accepted by the customer","description":"Records that the customer said yes, as \"Angenommen\" in craftivo. The\ntime is always now. No request body: a body with any field, for\nexample `date` or `reason`, answers 400.\n\nOnly from status `sent` or `customer_review`. Requires a key with\n`read_write` scope and the `projects: full` permission of the key's\ncreator (otherwise 403 `forbidden`). No extra right is needed.\n\nOnly the document changes. The customer request column and the\nproject status stay as they are; move them with their own requests.\nThe customer link with signature is a second way to accept and stays\nunchanged.\n\nWebhooks fire as in craftivo: `quote.updated` and\n`quote.accepted`. The history names the API key. Accepting an\nalready accepted quote answers 200 without a second history entry\nor webhook.\n\nErrors: unknown, deleted or inaccessible document or another document\ntype → 404; draft → 409 `document_not_finalized`; finalized but not\nsent → 409 `document_not_sent`; rejected, superseded or archived →\n409 `invalid_status_transition`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Accepted","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StatusConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes/{quoteId}/reject":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"rejectQuote","tags":["Documents"],"summary":"Mark a quote as rejected by the customer","description":"Records that the customer said no, as \"Nicht angenommen\" in craftivo.\nThe time is always now. `rejectionReason` comes from the fixed list\nof craftivo; `rejectionComment` is optional.\n\nFrom status `finalized`, `sent` or `customer_review`. Requires a key\nwith `read_write` scope and the `financials` permission of the key's\ncreator (otherwise 403 `forbidden`). No extra right is needed.\n\nWebhooks fire as in craftivo: `quote.updated` and\n`quote.rejected`. The history names the API key and the reason.\nRejecting an already rejected quote answers 200 without a second\nhistory entry or webhook and keeps the first reason.\n\nErrors: unknown, deleted or inaccessible document or another document\ntype → 404; draft → 409 `document_not_finalized`; accepted,\nsuperseded or archived → 409 `invalid_status_transition`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRejectRequest"}}}},"responses":{"200":{"description":"Rejected","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StatusConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/quotes/{quoteId}/order-confirmation":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"quoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"createOrderConfirmationFromQuote","tags":["Documents"],"summary":"Create an order confirmation draft from a quote","description":"Copies the quote into a new order confirmation, as \"In\nAuftragsbestätigung umwandeln\" in craftivo. The order confirmation\nis a draft without a number in the same project; finalize it with\n`POST /order-confirmations/{orderConfirmationId}/finalize`. The\nquote stays as it is and does not need to be accepted. No request\nbody.\n\nRequires a key with `read_write` scope and the `projects` and\n`create_offer` permissions of the key's creator (otherwise 403\n`forbidden`). No webhook fires for the draft.\n\nEvery call creates a new draft, as in craftivo. Send an\n`Idempotency-Key`: a retry with the same key returns the first\nresponse and creates no second draft.\n\nErrors: unknown, deleted or inaccessible quote, a cost estimate or\nanother document type → 404; draft → 409 `document_not_finalized`;\nrejected or archived quote or an addendum → 409\n`document_not_convertible`; quote without customer → 422\n`customer_required`; recipient party of another property → 422\n`recipient_party_not_on_property`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"201":{"description":"Order confirmation draft created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderConfirmationResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StatusConflict"},"422":{"$ref":"#/components/responses/CustomerRequired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/cost-estimates":{"get":{"operationId":"listCostEstimates","tags":["Documents"],"summary":"List cost estimates, newest first","description":"Only documents the key may read are listed (see the Documents tag). Filters and this rule can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/DocumentStatus"},{"$ref":"#/components/parameters/DocumentCustomerId"},{"$ref":"#/components/parameters/DocumentProjectId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/DocumentDateFrom"},{"$ref":"#/components/parameters/DocumentDateTo"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/cost-estimates/{costEstimateId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"costEstimateId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getCostEstimate","tags":["Documents"],"summary":"Get a cost estimate including its positions","description":"Returns 404 for a cost estimate the key may not read (see the Documents tag).","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/cost-estimates/{costEstimateId}/finalize":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"costEstimateId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"finalizeCostEstimate","tags":["Documents"],"summary":"Finalize a draft cost estimate","description":"Assigns the next number from the organization's number sequence and\nlocks the cost estimate, as \"Festschreiben\" in craftivo. Afterwards it can\nonly be corrected or cancelled in craftivo; `PATCH` answers 409.\n\nRequires the extra right `documents:finalize` of the API key\n(otherwise 403 `insufficient_scope`, `details.requiredScope`) and the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). No request body.\n\nThe response is the cost estimate in the detail format with `number`.\n`pdfUrl` is `null`: craftivo creates the archived PDF in the\nbackground right after finalizing. Read the document again to get\n`pdfUrl` once it exists. Webhooks fire as\nfor finalizing in craftivo: `cost_estimate.created` and\n`cost_estimate.finalized`. The history names the API key.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same number. Without a key a retry answers 409\n`document_finalized`.\n\nErrors: unknown, deleted or inaccessible document, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; not a\ndraft → 409 `document_finalized`; someone else is editing it in\ncraftivo → 409 `document_locked`; missing information → 422\n`finalization_incomplete` with `details.missingFields`; too many\nparallel finalizations in the organization → 429 with `Retry-After`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/FinalizationIncomplete"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/cost-estimates/{costEstimateId}/send":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"costEstimateId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"sendCostEstimate","tags":["Documents"],"summary":"Email a finalized cost estimate to the customer","description":"Sends the archived PDF of the cost estimate (with XRechnung also the XML\nfile) from the organization's sender mailbox, as \"Per E-Mail\nversenden\" in craftivo.\n\nRequires the extra right `documents:send` of the API key (otherwise\n403 `insufficient_scope`) and the `mailbox` and `financials`\npermissions of the key's creator (otherwise 403 `forbidden`).\n\nRecipient: the delivery address frozen when the document was\nfinalized, e.g. the property management of the building. craftivo\nnever falls back to the customer's email. `to`, `cc` and `bcc`\nreplace it.\n\nUse `dryRun: true` to see the email first. Send an\n`Idempotency-Key`: a retry returns the first response and never sends\na second email. A document that was already sent needs\n`resend: true`. The webhook `cost_estimate.sent` fires only when the\nstatus changes from finalized to sent, not for a resend.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendRequest"}}}},"responses":{"200":{"description":"Sent, or the preview for `dryRun`","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SendConflict"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/SendFailed"}}}},"/api/v1/cost-estimates/{costEstimateId}/accept":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"costEstimateId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"acceptCostEstimate","tags":["Documents"],"summary":"Mark a sent cost estimate as accepted by the customer","description":"Records that the customer said yes, as \"Angenommen\" in craftivo. The\ntime is always now. No request body: a body with any field, for\nexample `date` or `reason`, answers 400.\n\nOnly from status `sent` or `customer_review`. Requires a key with\n`read_write` scope and the `projects: full` permission of the key's\ncreator (otherwise 403 `forbidden`). No extra right is needed.\n\nOnly the document changes. The customer request column and the\nproject status stay as they are; move them with their own requests.\nThe customer link with signature is a second way to accept and stays\nunchanged.\n\nWebhooks fire as in craftivo: `cost_estimate.updated` and\n`cost_estimate.accepted`. The history names the API key. Accepting an\nalready accepted cost estimate answers 200 without a second history entry\nor webhook.\n\nErrors: unknown, deleted or inaccessible document or another document\ntype → 404; draft → 409 `document_not_finalized`; finalized but not\nsent → 409 `document_not_sent`; rejected, superseded or archived →\n409 `invalid_status_transition`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Accepted","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StatusConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/cost-estimates/{costEstimateId}/reject":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"costEstimateId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"rejectCostEstimate","tags":["Documents"],"summary":"Mark a cost estimate as rejected by the customer","description":"Records that the customer said no, as \"Nicht angenommen\" in craftivo.\nThe time is always now. `rejectionReason` comes from the fixed list\nof craftivo; `rejectionComment` is optional.\n\nFrom status `finalized`, `sent` or `customer_review`. Requires a key\nwith `read_write` scope and the `financials` permission of the key's\ncreator (otherwise 403 `forbidden`). No extra right is needed.\n\nWebhooks fire as in craftivo: `cost_estimate.updated` and\n`cost_estimate.rejected`. The history names the API key and the reason.\nRejecting an already rejected cost estimate answers 200 without a second\nhistory entry or webhook and keeps the first reason.\n\nErrors: unknown, deleted or inaccessible document or another document\ntype → 404; draft → 409 `document_not_finalized`; accepted,\nsuperseded or archived → 409 `invalid_status_transition`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentRejectRequest"}}}},"responses":{"200":{"description":"Rejected","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostEstimateResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StatusConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/order-confirmations":{"get":{"operationId":"listOrderConfirmations","tags":["Documents"],"summary":"List order confirmations, newest first","description":"Only documents the key may read are listed (see the Documents tag). Filters and this rule can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/DocumentStatus"},{"$ref":"#/components/parameters/DocumentCustomerId"},{"$ref":"#/components/parameters/DocumentProjectId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/DocumentDateFrom"},{"$ref":"#/components/parameters/DocumentDateTo"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderConfirmationListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/order-confirmations/{orderConfirmationId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"orderConfirmationId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getOrderConfirmation","tags":["Documents"],"summary":"Get an order confirmation including its positions","description":"Returns 404 for a order confirmation the key may not read (see the Documents tag).","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderConfirmationResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/order-confirmations/{orderConfirmationId}/finalize":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"orderConfirmationId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"finalizeOrderConfirmation","tags":["Documents"],"summary":"Finalize a draft order confirmation","description":"Assigns the next number from the organization's number sequence and\nlocks the order confirmation, as \"Festschreiben\" in craftivo. Afterwards it can\nonly be corrected or cancelled in craftivo; `PATCH` answers 409.\n\nRequires the extra right `documents:finalize` of the API key\n(otherwise 403 `insufficient_scope`, `details.requiredScope`) and the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). No request body.\n\nThe response is the order confirmation in the detail format with `number`.\n`pdfUrl` is `null`: craftivo creates the archived PDF in the\nbackground right after finalizing. Read the document again to get\n`pdfUrl` once it exists. Webhooks fire as\nfor finalizing in craftivo: `order_confirmation.created` and\n`order_confirmation.finalized`. The history names the API key.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same number. Without a key a retry answers 409\n`document_finalized`.\n\nErrors: unknown, deleted or inaccessible document, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; not a\ndraft → 409 `document_finalized`; someone else is editing it in\ncraftivo → 409 `document_locked`; missing information → 422\n`finalization_incomplete` with `details.missingFields`; too many\nparallel finalizations in the organization → 429 with `Retry-After`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderConfirmationResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/FinalizationIncomplete"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/order-confirmations/{orderConfirmationId}/send":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"orderConfirmationId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"sendOrderConfirmation","tags":["Documents"],"summary":"Email a finalized order confirmation to the customer","description":"Sends the archived PDF of the order confirmation (with XRechnung also the XML\nfile) from the organization's sender mailbox, as \"Per E-Mail\nversenden\" in craftivo.\n\nRequires the extra right `documents:send` of the API key (otherwise\n403 `insufficient_scope`) and the `mailbox` and `financials`\npermissions of the key's creator (otherwise 403 `forbidden`).\n\nRecipient: the delivery address frozen when the document was\nfinalized, e.g. the property management of the building. craftivo\nnever falls back to the customer's email. `to`, `cc` and `bcc`\nreplace it.\n\nUse `dryRun: true` to see the email first. Send an\n`Idempotency-Key`: a retry returns the first response and never sends\na second email. A document that was already sent needs\n`resend: true`. The webhook `order_confirmation.sent` fires only when the\nstatus changes from finalized to sent, not for a resend.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendRequest"}}}},"responses":{"200":{"description":"Sent, or the preview for `dryRun`","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SendConflict"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/SendFailed"}}}},"/api/v1/delivery-notes":{"get":{"operationId":"listDeliveryNotes","tags":["Documents"],"summary":"List delivery notes, newest first","description":"Only documents the key may read are listed (see the Documents tag). Filters and this rule can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/DocumentStatus"},{"$ref":"#/components/parameters/DocumentCustomerId"},{"$ref":"#/components/parameters/DocumentProjectId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/DocumentDateFrom"},{"$ref":"#/components/parameters/DocumentDateTo"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryNoteListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/delivery-notes/{deliveryNoteId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"deliveryNoteId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getDeliveryNote","tags":["Documents"],"summary":"Get a delivery note including its positions","description":"Returns 404 for a delivery note the key may not read (see the Documents tag).","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryNoteResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/delivery-notes/{deliveryNoteId}/finalize":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"deliveryNoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"finalizeDeliveryNote","tags":["Documents"],"summary":"Finalize a draft delivery note","description":"Assigns the next number from the organization's number sequence and\nlocks the delivery note, as \"Festschreiben\" in craftivo. Afterwards it can\nonly be corrected or cancelled in craftivo; `PATCH` answers 409.\n\nRequires the extra right `documents:finalize` of the API key\n(otherwise 403 `insufficient_scope`, `details.requiredScope`) and the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). No request body.\n\nThe response is the delivery note in the detail format with `number`.\n`pdfUrl` is `null`: craftivo creates the archived PDF in the\nbackground right after finalizing. Read the document again to get\n`pdfUrl` once it exists. Webhooks fire as\nfor finalizing in craftivo: `delivery_note.created` and\n`delivery_note.finalized`. The history names the API key.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same number. Without a key a retry answers 409\n`document_finalized`.\n\nErrors: unknown, deleted or inaccessible document, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; not a\ndraft → 409 `document_finalized`; someone else is editing it in\ncraftivo → 409 `document_locked`; missing information → 422\n`finalization_incomplete` with `details.missingFields`; too many\nparallel finalizations in the organization → 429 with `Retry-After`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryNoteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/FinalizationIncomplete"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/delivery-notes/{deliveryNoteId}/send":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"deliveryNoteId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"sendDeliveryNote","tags":["Documents"],"summary":"Email a finalized delivery note to the customer","description":"Sends the archived PDF of the delivery note (with XRechnung also the XML\nfile) from the organization's sender mailbox, as \"Per E-Mail\nversenden\" in craftivo.\n\nRequires the extra right `documents:send` of the API key (otherwise\n403 `insufficient_scope`) and the `mailbox` and `financials`\npermissions of the key's creator (otherwise 403 `forbidden`).\n\nRecipient: the delivery address frozen when the document was\nfinalized, e.g. the property management of the building. craftivo\nnever falls back to the customer's email. `to`, `cc` and `bcc`\nreplace it.\n\nUse `dryRun: true` to see the email first. Send an\n`Idempotency-Key`: a retry returns the first response and never sends\na second email. A document that was already sent needs\n`resend: true`. The webhook `delivery_note.sent` fires only when the\nstatus changes from finalized to sent, not for a resend.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendRequest"}}}},"responses":{"200":{"description":"Sent, or the preview for `dryRun`","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SendConflict"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/SendFailed"}}}},"/api/v1/invoices":{"post":{"operationId":"createInvoice","tags":["Documents"],"summary":"Create an invoice or partial invoice draft from a quote, order confirmation, maintenance visit or project","description":"Copies a finalized quote or order confirmation into a new standard\ninvoice, as \"In Rechnung umwandeln\" in craftivo: totals, positions,\nsections and measurements (\"Aufmaß\") of the source, the customer, the\nproject and the recipient. The invoice is a draft without a number;\nfinalize it with `POST /invoices/{invoiceId}/finalize`. The source\nstays as it is. No webhook fires for the draft; `invoice.created`\nfires on finalizing. The history names the API key.\n\n`serviceDate` or `servicePeriod` sets the service time of the new draft,\nfor every invoice type. Without them the draft has no service time; a new\ndraft does not take one over from its source. A retry with the same key\nreturns the first response; these two fields are not compared. A\nmaintenance visit that is already billed (200) keeps its invoice as it is.\n\n**`Idempotency-Key` is required on this route** (otherwise 400\n`idempotency_key_required`), and only on this route: a second invoice\nfor the same work is a bookkeeping (GoBD) problem. A retry with the\nsame key and the same source returns the first response and creates\nno second invoice. If that draft was deleted in craftivo in the\nmeantime, the retry answers 404 and creates nothing; send a new key\nfor a new invoice. The key applies per source: the same key with\nanother source creates that source's invoice. A new key creates a new\ninvoice, as a second click in craftivo does.\n\nRequires a key with `read_write` scope and the `financials` and\n`create_invoice` permissions of the key's creator (otherwise 403\n`forbidden`).\n\nErrors: `invoiceType` other than `standard`, `partial`, `down_payment` or `final`\n→ 400 `unsupported_invoice_type`; unknown, deleted or inaccessible source,\na source in a deleted project, a source of another document type\n(for example a cost estimate) or a retry with the same key after the\ninvoice draft was deleted → 404; source is a draft, rejected,\narchived, cancelled or superseded → 409 `invalid_source_status`;\naddendum or part of a collective invoice → 409\n`source_not_invoiceable`; source without customer → 422\n`customer_required`; recipient party of another property → 422\n`recipient_party_not_on_property`.\n\n**Maintenance visit** (`source.type: maintenance_visit`): bills the\nvisit as \"Rechnung erstellen\" in craftivo's maintenance overview. A\nnew order \"Wartungsabrechnung <contract number>\" carries the draft;\npositions come from the contract's flat rate or catalog positions\nminus the positions deselected at the visit, plus recorded material\nand extra hours. Visits billed after the visit must be completed;\nvisits billed in advance can be billed from their due date. Once the\nadvance invoice exists, a completed visit with recorded extras gets a\nfollow-up invoice for the extras. If the visit is already billed and\nthe invoice draft still exists, the response is 200 with that invoice\nand nothing is created. Additionally requires the maintenance\ncontracts plan feature (otherwise 403 `plan_feature_required`), the\n`maintenance:full` permission and the permission to create orders.\nErrors: unknown or deleted visit, its contract deleted, or a retry\nwith the same key after the invoice draft was deleted → 404;\nvisit not billable → 409 `visit_not_billable` with `details.reason`:\n`not_completed`, `advance_not_due` (the message names the due date),\n`nothing_to_bill`, `customer_deleted`, `property_deleted` or\n`recipient_party_inactive` (the recipient party chosen on the\ncontract, for example the property management, has ended or does not\nbelong to the contract's property; nothing is created).\n\n**Partial invoice** (`invoiceType: partial`): creates a draft with the\nsame lines, amounts and tax as \"Abschlagsrechnung\" in craftivo; the mode\nis set in `partialInvoice`. It has no number until\n`POST /invoices/{invoiceId}/finalize`, and `PATCH` does not change it\n(409 `unsupported_invoice_type`). Requires the same permissions as above,\nand access to the project. With `free_amount` the project takes the place\nof the source: unknown, deleted or inaccessible project → 404, project\nwithout customer → 422 `customer_required`. The recipient is the customer\nof the project; a draft from an order takes the recipient of the source.\n\nA lock of the order answers 409 `partial_invoice_conflict` and creates\nnothing; `message` gives the reason in German. This includes: a mode\nother than the one fixed by the first finalized partial invoice\n(`lump_sum` against the deprecated `selections`; `progress` mixes with\nboth), positions of an order with a section lump sum, and `free_amount`\nin a project with an order. Partial invoices above 100 % of the order\ntotal and a quantity above the rest of the position are allowed: with\nunit prices, extra quantities are part of the contract. Finalizing\nchecks the mode lock again and answers 409 `conflict`. With\n`selections`, it also answers 409 `conflict` if another partial invoice\nwas finalized after this draft was created and a fixed discount of the\norder would be deducted more than once; create the draft again.\n\nA retry with the same key, source and `partialInvoice` returns the first\nresponse if the caller still has the permissions and the project access\n(otherwise 403 or 404), and 404 if the draft was deleted. The same key and\nsource with other values answers 422 `idempotency_key_reused`.\n\n**Final invoice** (`invoiceType: final`): creates a draft with all lines\nof the source and the same totals as \"Schlussrechnung\" in craftivo. A\nposition that finalized partial invoices billed above the order quantity\ntakes the billed quantity. The draft keeps the quantities billed at\ncreation. If later partial invoices or cancellations would change a\nquantity it took over (this includes a corrected copy), finalizing\nanswers 409 `conflict`; delete the draft and create it again.\nIt has\nno number until `POST /invoices/{invoiceId}/finalize`, and `PATCH` does\nnot change it (409 `unsupported_invoice_type`). The response has\n`settlement` with the deductions and the remaining amount, also on\n`GET /invoices/{invoiceId}` and on finalize. Requires the same permissions\nas above, and access to the project. `settlement` is `null` on `GET`\nwithout financial rights.\n\nA lock answers 409 `final_invoice_conflict` and creates nothing; `message`\ngives the reason in German: an active final invoice already covers this\norder or project, a partial invoice in the same scope is still a draft,\nor with exactly one order any partial invoice of the project\n(finalize or delete it first; this lock exists only in the API, craftivo\ncreates the draft), the source has no project, or craftivo would not offer\nthis source (use the order confirmation; a quote only if accepted and the\nproject has no order confirmation). As in craftivo, a second draft is\nallowed while the first is one; finalizing the second answers 409\n`conflict`.\n\nA retry follows the same rules as a partial invoice.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"header","name":"Idempotency-Key","required":true,"description":"Required. Replays the stored response of an earlier request with\nthe same key, so a retry creates no second invoice. Stored\nresponses are kept for 30 days.\n","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInvoiceInput"}}}},"responses":{"200":{"description":"Maintenance visit already billed. The existing invoice draft is\nreturned; nothing is created.\n","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"201":{"description":"Invoice draft created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/InvoiceConflict"},"422":{"$ref":"#/components/responses/InvoiceCreateUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listInvoices","tags":["Documents"],"summary":"List invoices, credit notes, and down payments, newest first","description":"Each entry includes its `payments` (with skonto), so payment polling works without fetching every invoice individually. Positions remain detail-only. Only invoices the key may read are listed (see the Documents tag). Filters and this rule can make a page shorter than `limit`, or empty, while `meta.isDone` is `false`; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/DocumentStatus"},{"$ref":"#/components/parameters/InvoiceType"},{"$ref":"#/components/parameters/DocumentCustomerId"},{"$ref":"#/components/parameters/DocumentProjectId"},{"$ref":"#/components/parameters/UpdatedAfter"},{"$ref":"#/components/parameters/DocumentDateFrom"},{"$ref":"#/components/parameters/DocumentDateTo"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/invoices/{invoiceId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"invoiceId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getInvoice","tags":["Documents"],"summary":"Get an invoice including positions and payments","description":"Returns 404 for a invoice the key may not read (see the Documents tag).","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateInvoice","tags":["Documents"],"summary":"Change a draft standard invoice","description":"Changes the header, replaces the positions (`positions`) or adds\npositions (`appendPositions`) of a draft standard invoice. Fields and\nchecks are the same as for `PATCH /api/v1/quotes/{quoteId}`. Source,\nproject, customer and invoice type stay as they are; `projectId` and\n`invoiceType` are not accepted (400). Totals and history are the same\nas for a change in craftivo; the history names the API key. No webhook\nfires for a draft.\n\nPosition ids are not stable: every change saves all positions anew,\nso ids read before a PATCH are invalid afterwards. Read them again\nfrom the response. Measurements (\"Aufmaß\") of unchanged positions stay.\n\nRequires a key with `read_write` scope and the `financials`\npermission of the key's creator (otherwise 403 `forbidden`).\n\nErrors: unknown, deleted or inaccessible invoice or another document\ntype → 404; invoice no longer a draft → 409 `document_finalized`;\npartial, final invoice, credit note or down payment → 409\n`unsupported_invoice_type`; someone else is editing it in craftivo →\n409 `document_locked`; `version` older than the stored one → 409\n`version_conflict`. Recipient and delivery checks are the same as for\nquotes: with `deliveryEmailOverride` set, no delivery party is\nchecked, so an ended delivery party does not block the change.\n\n`serviceDate` or `servicePeriod` replaces the service time of the draft;\n`null` in either field removes it. Without a service time, finalizing\napplies the rule described there.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateInvoiceInput"}}}},"responses":{"200":{"description":"Updated","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/InvoiceConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/invoices/{invoiceId}/finalize":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"invoiceId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"finalizeInvoice","tags":["Documents"],"summary":"Finalize a draft invoice","description":"Assigns the next number from the organization's number sequence and\nlocks the invoice, as \"Festschreiben\" in craftivo. Afterwards it can\nonly be corrected or cancelled in craftivo; `PATCH` answers 409.\n\nRequires the extra right `documents:finalize` of the API key\n(otherwise 403 `insufficient_scope`, `details.requiredScope`) and the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). No request body.\n\nThe response is the invoice in the detail format with `number`.\n`pdfUrl` is `null`: craftivo creates the archived PDF in the\nbackground right after finalizing. Read the document again to get\n`pdfUrl` once it exists. Webhooks fire as\nfor finalizing in craftivo: `invoice.created` and\n`invoice.finalized`. The history names the API key.\n\nAn invoice needs a service date or service period (§ 14 UStG). If it\nhas only the start or only the end of a period, that date becomes the\nservice date; `warnings` holds `service_date_from_partial_period`. If it\nhas no date at all, craftivo sets the service date to the invoice date;\n`warnings` holds `service_date_defaulted`. Either way craftivo stores the\ndate with the number, answers 200 and `serviceDate` holds it. A partial\nor down payment invoice bills a service that is not performed yet: with\nno date at all it gets no service date (`serviceDate` and\n`servicePeriod` stay `null`, no warning), and its PDF and e-invoice\nstate that the service is still outstanding. An invoice finalized\nbefore keeps what it has. Set `serviceDate` or `servicePeriod`\non the draft with `POST /invoices` or `PATCH /invoices/{invoiceId}` to\navoid the warning.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same number. Without a key a retry answers 409\n`document_finalized`.\n\nErrors: unknown, deleted or inaccessible document, a legacy quote\nuploaded as PDF (\"Altangebot\") or another document type → 404; not a\ndraft → 409 `document_finalized`; someone else is editing it in\ncraftivo → 409 `document_locked`; missing information → 422\n`finalization_incomplete` with `details.missingFields`; too many\nparallel finalizations in the organization → 429 with `Retry-After`.\n\nA final invoice must deduct every partial invoice with a payment that\nit replaces. If such a partial invoice was deselected in craftivo, the\nrequest answers 409 `conflict`, assigns no number and `message` names\nthe partial invoice in German. Partial invoices of another order in the\nproject stay optional.\n\nWith `offsetScope: source`, a final invoice also deducts and replaces\npartial invoices on the quote of its order confirmation, if that quote\nhas no other active order confirmation. If the quote has several and a\npartial invoice on it carries a payment, the request answers 409\n`conflict` and `message` names the scope `project` in German as\n„Gesamtes Projekt“.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalized","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceFinalizedResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/FinalizationIncomplete"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/invoices/{invoiceId}/cancel":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"invoiceId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"cancelInvoice","tags":["Documents"],"summary":"Cancel a finalized invoice with a cancellation invoice","description":"Cancels the invoice as \"Stornieren\" in craftivo: craftivo creates a\nfinalized cancellation invoice (`type: credit_note`) with a number from\nits own number sequence and every position negated. The original gets\nthe status `cancelled` and keeps its number. Works for standard,\npartial, down payment and final invoices. Cancelling a final invoice\nreopens the partial invoices it replaced. The cancellation cannot be\nundone; create a new invoice instead.\n\nRequires the extra right `documents:finalize` of the API key, because\nthe cancellation invoice gets a number like finalizing (otherwise 403\n`insufficient_scope`, `details.requiredScope`), and the `financials`\npermission of the key's creator (otherwise 403 `forbidden`).\n\n`reason` is required and appears in the history of the invoice. The\nresponse is the cancellation invoice in the detail format;\n`relatedInvoiceId` names the cancelled invoice. `pdfUrl` is `null`:\ncraftivo archives the PDF in the background. Webhooks fire as for\ncancelling in craftivo: `invoice.updated` and `invoice.cancelled` for\nthe original, `invoice.created` and `invoice.finalized` for the\ncancellation invoice. The history names the API key.\n\n**Booked payments block the cancellation.** Correct them to 0 first\nwith `POST /invoices/{invoiceId}/payments/corrections`, then cancel\nand book the payment on the new invoice. Otherwise the request answers\n409 `invoice_has_payments`; `message` explains this in German.\n\nSend an `Idempotency-Key`: a retry after a timeout returns the first\nresponse with the same cancellation invoice. Without a key a retry\nanswers 409 `cancellation_conflict`.\n\nErrors: missing `reason`, fewer than 10 or more than 1,000\ncharacters, or another field → 400 `invalid_body`; unknown, deleted or\ninaccessible invoice or another document type → 404; a draft, a\ncancellation invoice, an invoice that is already cancelled or a\npartial invoice replaced by a final invoice → 409\n`cancellation_conflict` with a German `message`; too many parallel\nrequests in the organization → 429 with `Retry-After`.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceCancelRequest"}}}},"responses":{"201":{"description":"Invoice cancelled. The body is the new cancellation invoice.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InvoiceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/invoices/{invoiceId}/send":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"invoiceId","required":true,"schema":{"type":"string"}}],"post":{"operationId":"sendInvoice","tags":["Documents"],"summary":"Email a finalized invoice to the customer","description":"Sends the archived PDF of the invoice (with XRechnung also the XML\nfile) from the organization's sender mailbox, as \"Per E-Mail\nversenden\" in craftivo. Credit notes and down payment invoices are sent here too; dunning letters are not.\n\nRequires the extra right `documents:send` of the API key (otherwise\n403 `insufficient_scope`) and the `mailbox` and `financials`\npermissions of the key's creator (otherwise 403 `forbidden`).\n\nRecipient: the delivery address frozen when the document was\nfinalized, e.g. the property management of the building. craftivo\nnever falls back to the customer's email. `to`, `cc` and `bcc`\nreplace it.\n\nUse `dryRun: true` to see the email first. Send an\n`Idempotency-Key`: a retry returns the first response and never sends\na second email. A document that was already sent needs\n`resend: true`. The webhook `invoice.sent` fires only when the\nstatus changes from finalized to sent, not for a resend.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendRequest"}}}},"responses":{"200":{"description":"Sent, or the preview for `dryRun`","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentSendResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SendConflict"},"422":{"$ref":"#/components/responses/SendUnprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"},"502":{"$ref":"#/components/responses/SendFailed"}}}},"/api/v1/invoices/{invoiceId}/payments":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"invoiceId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"listInvoicePayments","tags":["Documents"],"summary":"List the payments of an invoice","description":"All payments and corrections of the invoice, oldest first, without\npagination. A credit note returns an empty list. Requires the\n`financials` permission of the key's creator (otherwise 403\n`forbidden`). Unknown, deleted or inaccessible invoice → 404.\n","responses":{"200":{"description":"Payments","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"recordInvoicePayment","tags":["Documents"],"summary":"Record a payment on an invoice","description":"Books a payment as \"Zahlung erfassen\" in craftivo: the invoice becomes\n`partially_paid` or `paid`, the payment appears at the invoice, the\nhistory names the API key and the bell notifies the finance team.\n\nRequires the extra right `payments:write` of the API key (otherwise\n403 `insufficient_scope`, `details.requiredScope`), and the\n`financials` permission and full project access (`projects: full`)\nof the key's creator (otherwise 403 `forbidden`).\n\n**Never books twice.** `externalId` is required and unique per\ninvoice. The same `externalId` with the same data answers 200 with\nthe existing payment and the current invoice state, also when the\ninvoice is paid by now. The same `externalId` with different data\nanswers 409 `external_id_conflict`. The `Idempotency-Key` header is\noptional; with it a retry returns the first response. Unlike other\nroutes, this route compares the payment: the same key with a\ndifferent payment answers 422 `idempotency_key_reused` and books\nnothing.\n\nAmounts are integer cents; `amountCents` plus `skontoAppliedCents`\nmust not exceed the open amount. An invoice is `paid` when they\nreach the open amount exactly; there is no tolerance. Skonto after\nthe deadline, without skonto terms or above the agreed rate is\nbooked with a warning in `warnings`, as in craftivo.\n\nWebhooks: `payment.created`, `invoice.updated`, and\n`invoice.partially_paid` or `invoice.paid` when the status changes.\nTo reverse a payment, book a correction with\n`POST /api/v1/invoices/{invoiceId}/payments/corrections`.\n\nErrors: invalid amount, date or method, missing `externalId` → 400\n`invalid_body`; unknown, deleted or inaccessible invoice → 404; see\n409 for the payment rules.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentCreateRequest"}}}},"responses":{"200":{"description":"A payment with this `externalId` already exists with the same data. Nothing was booked.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecordResponse"}}}},"201":{"description":"Payment recorded","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecordResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PaymentConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/invoices/{invoiceId}/payments/corrections":{"parameters":[{"name":"invoiceId","in":"path","required":true,"schema":{"type":"string"}},{"$ref":"#/components/parameters/OrganizationId"}],"post":{"operationId":"recordInvoicePaymentCorrection","tags":["Documents"],"summary":"Correct a payment with a reversing entry","description":"Books \"Korrekturbuchung\" as in craftivo: a new line with a negative\namount. The original payment stays unchanged; payments are never\nedited or deleted. The invoice status is calculated again: a `paid`\ninvoice can become `partially_paid`, and a full reversal returns it\nto its status before the payment.\n\nA correction applies to the paid amount of the invoice, not to one\npayment. The key can therefore also reverse payments booked in\ncraftivo or with another key.\n\nRequires the extra right `payments:correct` of the API key;\n`payments:write` alone is not enough (otherwise 403\n`insufficient_scope`, `details.requiredScope`). Also requires the\n`financials` permission and full project access (`projects: full`)\nof the key's creator (otherwise 403 `forbidden`).\n\n**Never books twice.** `externalId` is required and unique per\ninvoice, shared with its payments. The same `externalId` with the\nsame data answers 200 with the existing correction and the current\ninvoice state. The same `externalId` with different data, or the\n`externalId` of a payment, answers 409 `external_id_conflict`. The\n`Idempotency-Key` header is optional; with it a retry returns the\nfirst response. The same key with a different correction answers 422\n`idempotency_key_reused` and books nothing.\n\nWebhooks: `payment.created` with `payment.type: correction`,\n`invoice.updated`, and the invoice status event when the status\nchanges.\n\nErrors: positive or zero amount, missing or empty `note`, invalid\ndate or method, missing `externalId` → 400 `invalid_body`; unknown,\ndeleted or inaccessible invoice → 404; a correction beyond the paid\namount → 409 `settlement_negative`; see 409 for the other rules.\n","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentCorrectionCreateRequest"}}}},"responses":{"200":{"description":"A correction with this `externalId` already exists with the same data. Nothing was booked.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecordResponse"}}}},"201":{"description":"Correction booked","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentRecordResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PaymentConflict"},"422":{"$ref":"#/components/responses/IdempotencyKeyReused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/files":{"post":{"operationId":"uploadFile","tags":["Files"],"summary":"Upload a file for later use","description":"Send the raw file as the request body (no JSON, no multipart) with its\nMIME type in `Content-Type` and the name in the `fileName` query\nparameter. Returns a file id that can be referenced exactly once —\neither as the answer to a file field in `POST /api/v1/form-submissions`,\nvia `POST /api/v1/customer-requests/{customerRequestId}/files` or via\n`POST /api/v1/tasks/{taskId}/files`.\nUnused uploads are deleted after 24 hours. Maximum size is 20 MB;\nallowed types are common image formats (JPEG, PNG, WebP, GIF, HEIC),\nPDF, Word, Excel, CSV, and plain text. Uploads have their own quota of\n300 per organization and UTC day; a 429 from it carries\n`details.quota: \"file_uploads\"` and no `RateLimit-*` headers. A practice\naccount stores at most 1 GB; an upload beyond that returns 413\n`storage_limit_reached` with `details.maxStorageBytes`.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"query","name":"fileName","required":true,"description":"Display name of the file, at most 255 characters.","schema":{"type":"string","minLength":1,"maxLength":255}}],"requestBody":{"required":true,"content":{"*/*":{"schema":{"type":"string","format":"binary"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiFileResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"415":{"$ref":"#/components/responses/UnsupportedMediaType"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/forms":{"get":{"operationId":"listExternalForms","tags":["Forms"],"summary":"List active external forms and their public field definitions","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFormListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/form-submissions":{"get":{"operationId":"listExternalFormSubmissions","tags":["Forms"],"summary":"List non-spam form submissions, newest first","description":"Includes submissions from the website and this API. Technical request metadata and honeypot submissions are never returned.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFormSubmissionListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createExternalFormSubmission","tags":["Forms"],"summary":"Submit an active external form","description":"Uses the same customer mapping, sales routing, notifications, and webhook\nflow as the website. If the form has the \"Create customer request\" option\nenabled, the submission also creates a customer request on the Kanban\nboard, including the customer mapping — readable and editable via\n`/api/v1/customer-requests`. File fields take an array of file ids from\n`POST /api/v1/files` as their answer value (at most 5 per field, each\nid usable once). An invalid, foreign, or already used id returns\n`invalid_submission` with the field code `invalid_file`; a file id\nconsumed by a parallel request after validation returns HTTP 409\n(`conflict`).\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateExternalFormSubmissionInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalFormSubmissionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/form-templates":{"get":{"operationId":"listInternalFormTemplates","tags":["Forms"],"summary":"List active internal form templates and their field definitions","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalFormTemplateListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/form-instances":{"get":{"operationId":"listFormInstances","tags":["Forms"],"summary":"List internal form instances, newest first","description":"Includes project, customer-request, and maintenance contexts. Share URLs are returned only for customer-access instances; PDF URLs are short-lived and appear only when ready. An instance is listed only when `GET /api/v1/projects/{projectId}` shows its project to this key and, on a customer request, `GET /api/v1/customer-requests/{customerRequestId}` shows that request to this key, also without a project. Instances on deleted projects are never listed. A page can hold fewer rows than `limit`, or none, before `meta.isDone` is true; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormInstanceListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createFormInstance","tags":["Forms"],"summary":"Create a customer-access form instance and share URL","description":"Creates one shareable instance for a project or customer request. Maintenance protocol instances can be read here but are created only by the maintenance workflow. The share URL has no separate expiry; treat it like a secret. A project context, and the linked or converted project of a customer request context, must be a project that `GET /api/v1/projects/{projectId}` shows to this key; any other project answers 404 `form_context_not_found`, as for an unknown context. A customer request context must also be a request that `GET /api/v1/customer-requests/{customerRequestId}` shows to this key, also without a project. A repeated call with the same `Idempotency-Key` follows the same rules.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFormInstanceInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormInstanceResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/form-instance-submissions":{"get":{"operationId":"listInternalFormSubmissions","tags":["Forms"],"summary":"List internal form submissions as answer arrays","description":"Technical request metadata such as IP address and user agent is never returned. PDF URLs are short-lived and appear only when ready. A submission is listed only when its instance appears in `GET /api/v1/form-instances` for this key. A page can hold fewer rows than `limit`, or none, before `meta.isDone` is true; keep following `meta.cursor`.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalFormSubmissionListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createInternalFormSubmission","tags":["Forms"],"summary":"Submit answers to an open internal form instance","description":"Validates the answer IDs and values against the live template, marks the instance submitted, and starts PDF generation for project and customer-request contexts. An already submitted instance returns HTTP 409. An instance on a deleted project, or on a project that `GET /api/v1/projects/{projectId}` does not show to this key, or on a customer request that `GET /api/v1/customer-requests/{customerRequestId}` does not show to this key, also without a project, answers 404 `form_instance_not_found`, as for an unknown instance, and stays open. A repeated call with the same `Idempotency-Key` follows the same rule.","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInternalFormSubmissionInput"}}}},"responses":{"201":{"description":"Created","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InternalFormSubmissionResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/webhooks":{"get":{"operationId":"listWebhooks","tags":["Webhooks"],"summary":"List the webhook endpoints created through this API","description":"Lists only endpoints with `source: api`. A key of an owner (`inhaber`)\nsees every one of them, no matter which API key of the organization\ncreated it. Any other key sees only the endpoints its own member created.\nEndpoints created in the settings UI stay invisible here; the owner\nmanages those in craftivo.\n\nThe list is never paginated (an organization has at most 50 endpoints),\nso `meta.isDone` is always `true` and `meta.cursor` is empty.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createWebhook","tags":["Webhooks"],"summary":"Create a webhook endpoint","description":"The response contains the signing secret **exactly once**. Only public\nhttps targets are accepted; http, private, loopback and link-local\naddresses are rejected with HTTP 400. An organization can have at most\n50 endpoints (UI and API combined) — the 51st is rejected with HTTP 409\n(`webhook_limit_reached`).\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookInput"}}}},"responses":{"201":{"description":"Created. Contains the signing secret.","headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Idempotency-Replayed":{"$ref":"#/components/headers/IdempotencyReplayed"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecretResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"405":{"$ref":"#/components/responses/MethodNotAllowed"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/webhooks/{webhookId}":{"parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"in":"path","name":"webhookId","required":true,"schema":{"type":"string"}}],"get":{"operationId":"getWebhook","tags":["Webhooks"],"summary":"Get a webhook endpoint","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateWebhook","tags":["Webhooks"],"summary":"Update target, subscription or active state","description":"Re-activating an endpoint (`isActive: true`) also clears its failure\ncounter, so a recovered endpoint is not disabled again by old failures.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookInput"}}}},"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteWebhook","tags":["Webhooks"],"summary":"Delete a webhook endpoint","description":"Unsubscribes the endpoint. Deleting an already deleted endpoint returns\nHTTP 404 — Zapier and comparable clients treat both as \"unsubscribed\".\n","responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookDeletedResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/webhooks/{webhookId}/rotate-secret":{"post":{"operationId":"rotateWebhookSecret","tags":["Webhooks"],"summary":"Replace the signing secret","description":"Hard rotation without a grace period: from the response on, deliveries\nare signed with the new secret only. The new secret is returned exactly\nonce.\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"webhookId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"OK. Contains the new signing secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookWithSecretResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/api/v1/webhooks/{webhookId}/test":{"post":{"operationId":"testWebhook","tags":["Webhooks"],"summary":"Send a test delivery","description":"Queues a `webhook.test` event for this endpoint, regardless of its\nsubscription. A disabled endpoint is rejected with HTTP 409\n(`endpoint_disabled`).\n","parameters":[{"$ref":"#/components/parameters/OrganizationId"},{"$ref":"#/components/parameters/IdempotencyKey"},{"in":"path","name":"webhookId","required":true,"schema":{"type":"string"}}],"responses":{"202":{"headers":{"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"description":"Accepted. The delivery runs in the background.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookTestResponse"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/PaymentRequired"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/Conflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}