API changelog

Contract changes

New endpoints, changed fields, deprecations, and behavior changes that can affect an integration are recorded here.

The API remains under the v1 path. Breaking changes require an explicit migration path and will be announced here before they take effect.
  1. Partial invoices by progress
    v1
    • New: `partialInvoice.progress` on POST /api/v1/invoices with mode `positions`. Send the progress to date of each position, as `quantity` or as `percent` of the quantity in the source. The invoice bills the progress net minus the net amount of all finalized partial and down payment invoices of the order, paid or not, as "Abschlag nach Positionen" in craftivo. Tax is only charged on the difference.
    • Invoice responses of such a partial invoice have the new field `progressBilling`: the progress net, each deducted invoice with number, date, net amount and payment state, and the amount payable. It is `null` without the `financials` permission of the key's creator. GET /api/v1/invoices/{invoiceId} and finalize return it too.
    • Deprecated: `partialInvoice.selections` (quantities since the last partial invoice) since 2026-11-12, sunset 2027-06-01. Until then it works unchanged. Use `progress` instead.
    • Because a field of its request is deprecated, every response of POST /api/v1/invoices now carries the `Deprecation` header. There is no `Sunset` header: the endpoint stays.
  2. Clock marks on time entries
    v1
    • GET /api/v1/time-entries and every other response with a time entry carry `startTimeMark` and `endTimeMark`. They show when a start or end time comes from a phone and differs from the server time, like the columns "Beginn erfasst" and "Ende erfasst" of the time export.
    • A mark holds `kind` (`offline`: sent later without network; `deviceSkew`: the phone clock was more than 5 minutes off) and `serverTime`, when the time arrived at the server. `startTime` and `endTime` stay the phone time.
    • Time entries without a mark, also all entries from before, return `null`. The webhooks `time_entry.created`, `time_entry.updated` and `time_entry.deleted` carry the same fields.
  3. Partial invoices may bill extra quantities
    v1
    • POST /api/v1/invoices with `partialInvoice.mode` `positions` accepts a quantity above the remaining quantity of a position. Under a unit-price contract, extra quantities belong in the partial invoice. Before, such a request answered 409 `partial_invoice_conflict`.
    • A position that is fully billed can be billed again.
    • If the quantity billed so far exceeds the quantity of the order, the PDF shows below the position the quantity of the order and the quantity performed so far, for example „Auftrag: 20 m, bisher erbracht: 26 m“.
    • A fixed discount of the order is never deducted more than once in total over all partial invoices. If another partial invoice was finalized after a draft was created and the discount would be deducted twice, POST /api/v1/invoices/{invoiceId}/finalize answers 409 `conflict`; create the draft again.
    • A final invoice (`invoiceType: final`) takes for each position the larger quantity of the order and the finalized partial invoices, so extra quantities are not lost. If later partial invoices or cancellations would change a quantity the final invoice took over (also for a corrected copy), finalizing it answers 409 `conflict`; delete the draft and create it again.
  4. Partial invoices above the order total
    v1
    • POST /api/v1/invoices with `partialInvoice.mode: lump_sum` no longer answers 409 `partial_invoice_conflict` when all partial invoices together exceed 100 % of the order total. With unit prices, extra quantities are part of the contract; the final invoice deducts every partial invoice.
    • POST /api/v1/invoices/{invoiceId}/finalize no longer answers 409 for the same reason.
    • `partialInvoice.percent` accepts values above 100; a single partial invoice may exceed the order total.
  5. Cancel invoices
    v1
    • New: POST /api/v1/invoices/{invoiceId}/cancel cancels a finalized invoice as "Stornieren" in craftivo. It creates a numbered cancellation invoice (`type: credit_note`) and answers 201 with it; the original gets the status `cancelled`.
    • The request needs a `reason` of 10 to 1,000 characters, the extra right `documents:finalize` of the API key and the `financials` permission of its creator.
    • Booked payments block the cancellation with 409 `invoice_has_payments` and a German `message` that names the way out: correct the payments to 0 with POST /api/v1/invoices/{invoiceId}/payments/corrections, then cancel. Other states that cannot be cancelled answer 409 `cancellation_conflict`.
    • Send an `Idempotency-Key`: a retry returns the first response with the same cancellation invoice.
  6. Deprecation headers on webhook deliveries
    v1
    • A webhook delivery carries the `Deprecation` header (RFC 9745) when the event or a field of its payload is deprecated in the specification, like an API response.
    • The `Sunset` header (RFC 8594) is sent only when the whole event is deprecated and may go away, with the event's `x-sunset`. A deprecated payload field gets no `Sunset` header.
    • Nothing is deprecated today, so no delivery changes yet.
  7. Deprecated parts may be removed after their sunset date
    v1
    • A deprecated endpoint, parameter or field may be removed from v1 once its `x-sunset` has passed. Until then it keeps working.
    • Every such removal is listed here. Enum values have no deprecation marker, so none is removed, and no field changes its type.
    • Nothing is deprecated today, so nothing can be removed before May 2027.
  8. Sunset header only for deprecated endpoints
    v1
    • The `Sunset` header (RFC 8594) is sent only when the endpoint itself is deprecated, with the endpoint's `x-sunset`. It means the whole endpoint may go away on that date.
    • A deprecated parameter or field adds only the `Deprecation` header (RFC 9745) to the response. Its `x-sunset` stands in the specification.
    • Nothing is deprecated today, so no response changes yet.
  9. Partial and down payment invoices get no default service date
    v1
    • POST /api/v1/invoices/{invoiceId}/finalize no longer sets the invoice date as the service date of a partial or down payment invoice without any service date or period. `serviceDate` and `servicePeriod` stay `null`, and `warnings` does not hold `service_date_defaulted`.
    • Such an invoice bills a service that is not performed yet (§ 14 UStG, UStAE 14.5 para. 16 no. 5). Its PDF and e-invoice state this instead of a service date.
    • A partial or down payment invoice with only the start or only the end of a period still gets that date as the service date. Standard and final invoices and credit notes are unchanged.
  10. Time entries in the future are rejected
    v1
    • POST /api/v1/time-entries and PATCH /api/v1/time-entries/{entryId} answer 400 `invalid_body` when `startTime` lies more than 5 minutes or `endTime` more than 2 hours after the server time and the organization does not allow time entries in the future. This is the same rule as in the craftivo apps; the 2 hours cover the drive back to the company.
    • Time entries that end by then are unchanged.
  11. API webhooks belong to their creator or an owner
    v1
    • A webhook delivers data with the rights of the member who created it. GET, PATCH and DELETE /api/v1/webhooks/{webhookId}, POST /api/v1/webhooks/{webhookId}/rotate-secret and POST /api/v1/webhooks/{webhookId}/test now answer 404 when the key belongs to a member who neither created the webhook nor is currently an owner (`inhaber`).
    • GET /api/v1/webhooks lists every API webhook only for a key of an owner. Any other key gets only the webhooks its own member created.
    • Keys of owners and keys that manage their own webhooks are unchanged.
  12. A quote for a customer request no longer creates a project
    v1
    • POST /api/v1/customer-requests/{customerRequestId}/offers no longer creates a hidden draft project and no longer restores a deleted one. If the request has no project or order that is not deleted, the quote belongs only to the request and has `projectId: null`. No `project.created` is sent for it.
    • This is how a quote from a customer request works in craftivo. Before, a key without the `create_project` permission could create a project this way.
    • A request that already has a project or order is unchanged: the quote belongs to it.
    • POST /api/v1/customer-requests/{customerRequestId}/convert creates the project or order and moves the quote of the request to it.
  13. `time_entry.created` covers time entries recorded in craftivo
    v1
    • `time_entry.created` is now sent once for each time entry recorded in craftivo: entered after the fact, booked for a team, and the new entry of a correction of a locked time entry. Before, only time entries created through POST /api/v1/time-entries sent it.
    • A running timer still sends nothing. When it is stopped, by hand or automatically, it sends `time_entry.created`, because only now is it a time entry in GET /api/v1/time-entries.
    • A time entry that is reopened and stopped again sends `time_entry.updated`, not a second `time_entry.created`. Correcting the end of an automatically stopped timer with the stop button now sends `time_entry.updated` too.
    • The read rule is the same as before: an endpoint whose creator may not see the time entry only gets `timeEntry.id`.
  14. `schedule_entry.*` webhooks cover more ways to plan
    v1
    • `schedule_entry.created` is now also sent when a pre-planning or a project from the queue is released, one event for each assignment and in batches of 50 about one second apart; when a technician takes over open work; and for an appointment from a block of a service plan. A pre-planning bar still sends nothing.
    • `schedule_entry.updated` is now also sent when a technician takes over a scheduled maintenance visit, when a maintenance appointment moves, and when a cancelled date of a recurring assignment is restored. Changes to many assignments at once send one event for each of them, in batches of 50 about one second apart: moving the start of their project, taking a member off a project together with their assignments, and deactivating or removing a member.
    • `schedule_entry.deleted` is now also sent for assignments released from a deleted pre-planning, for those of the open visits of an archived maintenance contract, and for those of a deleted project once its undo period has ended. For a deleted project an endpoint only gets `scheduleEntry.id`.
    • Team notes, changing the members of a team and turning a maintenance visit into an order send no event yet.
  15. Webhook receivers can tell archived and deleted projects
    v1
    • Projects and orders in GET /api/v1/projects, GET /api/v1/projects/{projectId}, the responses of the other project routes and `project.*` webhook deliveries now have `archivedAt`: the time of archiving as an ISO date, otherwise `null`. A stored response or delivery from before this change does not have it.
    • New webhook event `project.deleted`: sent once when a project or order is deleted in craftivo, and when a customer request is marked as lost and its draft project is deleted with it. The payload is the public project as for `project.created`, as it was when deleted, so with `visibleOnBoard: false`. A wildcard subscription to `project.*` or `*` now receives it too, so check `event` before treating a delivery as a change.
    • A deleted project coming back now sends one `project.updated`: the undo after deleting, reopening a lost customer request whose draft project went with it, and using the deleted draft project of a customer request again. Before, nothing was sent.
    • Archiving still sends `project.updated`, now with `archivedAt` set; restoring sends it with `archivedAt: null`.
  16. Deleting a status column reports the moved projects and orders
    v1
    • Fix: Deleting a project or order status column moves its projects and orders to the chosen column and now sends one `project.updated` for each of them, also for deleted ones. Before, it sent nothing. The read rule of `project.created` applies.
    • A large column sends these in batches of 50 about one second apart, so they can arrive some time after the column is gone.
    • Fix: An order moved this way into a completed or cancelled column now has `status` `completed` or `cancelled`, and an order with one of these `status` values moved into another column goes back to `in_progress`, as when it is moved on the board. Before, `status` stayed unchanged.
  17. Set and read the service date or service period of an invoice
    v1
    • POST /api/v1/invoices and PATCH /api/v1/invoices/{invoiceId} accept `serviceDate` (`YYYY-MM-DD`) or `servicePeriod` (`from` and `to`, both `YYYY-MM-DD`). Send one of them, not both. The value replaces what the draft had. A new draft takes no service time from its source. On PATCH, `null` in either field removes the service time. PATCH still changes only draft standard invoices.
    • Invoices in GET /api/v1/invoices, GET /api/v1/invoices/{invoiceId}, the responses of POST, PATCH and finalize, and documents in `invoice.*` webhook deliveries now have `serviceDate` and `servicePeriod`. One holds the value, the other is `null`; both are `null` without a service time. A stored response or delivery from before this change does not have them.
    • An invoice with a service date or a complete period finalizes without a warning and keeps that value. Without one, finalizing still applies the invoice date with `service_date_defaulted`; half a period entered in craftivo shows a `null` bound and finalizes with `service_date_from_partial_period`.
  18. Forms on a customer request follow the customer request's access rule
    v1
    • Fix: GET /api/v1/form-instances, GET /api/v1/form-instance-submissions and POST /api/v1/form-instance-submissions now treat a form instance on a customer request like GET /api/v1/customer-requests/{customerRequestId} treats that request, also when the request has no project. Without full project rights, the key's creator needs a task or appointment from the office on the request, or a team row on its linked or converted project. Before, only that project was checked.
    • Hidden instances and submissions are left out of the lists. POST /api/v1/form-instance-submissions answers 404 `form_instance_not_found`, as for an unknown instance, and the instance stays open. A retry with the same `Idempotency-Key` answers the same 404 once access is gone.
    • In `form_instance.created` and `form_instance.submitted` webhooks, the same rule applies to the webhook's creator: other webhooks get neither the event nor an entry in their delivery log, and a waiting delivery is dropped when access ends.
  19. Time entries: moving an entry to a project without access answers 404
    v1
    • Fix: PATCH /api/v1/time-entries/{entryId} with a new `projectId` or `orderId` now answers 404 `not_found` when the key's creator has no access to that project, like an unknown id and like POST /api/v1/time-entries. Before, it answered 403 `forbidden`, or 400 for a completed project, and so confirmed that the project exists.
    • Access means on the project's team, scheduled on it in the planning board, or the full projects permission. With access, a completed project still answers 400.
    • An entry whose project stays the same can still be corrected, for example its break, even when the key's creator has lost access to the project.
  20. Time entries: the level `none` closes a member's own time too
    v1
    • Fix: A key whose creator has the `time_tracking` level `none` now gets 403 `forbidden` with `requiredPermission` `time_tracking:own` from GET /api/v1/time-entries and POST /api/v1/time-entries, also for their own time. Before, GET returned their own entries and POST created entries for them. PATCH and DELETE already answered this way.
    • A retry of POST /api/v1/time-entries with the same `Idempotency-Key` answers the same 403 once the level is `none`. The same applies to the app. Give the member the level `own` to record their own time again; no entry is lost.
    • In `time_entry.*` webhook deliveries, an endpoint whose creator has the level `none` keeps getting only the entry's `id`, as before.
  21. Schedule entries: no member twice on the same project at the same time
    v1
    • POST /api/v1/schedule-entries, PATCH /api/v1/schedule-entries/{scheduleEntryId} and POST /api/v1/maintenance-contracts/{contractId}/schedule-visits now answer 409 `schedule_duplicate` when a member would be scheduled for the same project at an overlapping time twice. Nothing is written and no one is notified. `force: true` does not override this, unlike `schedule_conflict`.
    • Completed and cancelled assignments do not count. A member on another project at the same time still gets `schedule_conflict`, which `force: true` overrides as before. Changing an assignment that was already scheduled twice before this change stays possible as long as no new overlap is created.
  22. Partially paid invoices become overdue after the due date
    v1
    • Fix: A partially paid invoice whose due date has passed now reports `isOverdue` `true` and the days in `overdueDays`. Before, it stayed `isOverdue` `false` and `overdueDays` 0 for good, and a partial payment on an overdue invoice removed the flag.
    • `status` stays `partially_paid`, so the partial payment stays visible. Check `isOverdue`, not `status` `overdue`, to find all overdue invoices.
    • `invoice.overdue` fires for a partially paid invoice when its due date passed within the last 7 days. Partially paid invoices whose due date passed earlier get the flag with an `invoice.updated` delivery only.
    • This applies to GET /api/v1/invoices, GET /api/v1/invoices/{invoiceId}, the `invoice` state of payment responses and `payment.created`, and documents in `invoice.*` webhook deliveries.
  23. Invoice drafts and archived invoices report 0 as open
    v1
    • Fix: `openAmountCents` is now 0 for an invoice draft (`status` `draft`) and an archived invoice (`status` `archived`). Before, both reported their full payable amount as open, so a sum of `openAmountCents` per customer showed receivables that do not exist. Cancelled and superseded invoices already reported 0.
    • This applies to GET /api/v1/invoices, GET /api/v1/invoices/{invoiceId}, the response of POST /api/v1/invoices and its retry with the same `Idempotency-Key`, the `invoice` state of payment responses and `payment.created`, and documents in `invoice.*` webhook deliveries. A stored response or delivery from before this change keeps its old value.
    • `grossAmountCents`, `payableAmountCents` and `paidAmountCents` stay the same. Without the `financials` permission of the key's or endpoint's creator, `openAmountCents` stays `null`.
    • You no longer need your own list of statuses to sum open receivables: add `openAmountCents` of all invoices.
  24. Retry of an invoice whose draft was deleted answers 404
    v1
    • Fix: POST /api/v1/invoices for a standard invoice from a quote or order confirmation, repeated with the same `Idempotency-Key` after the invoice draft was deleted in craftivo, now answers 404 `not_found` and creates nothing. Before, it answered 201 with the stored response of an invoice that no longer exists, while GET /api/v1/invoices/{invoiceId} answered 404.
    • Partial, final and maintenance invoices already answered 404 in this case. To create a new invoice, send a new key.
  25. 403 names the required level when the creator has the level none
    v1
    • Fix: A 403 `forbidden` for a request that needs a minimum level now names that level in `details.requiredPermission` also when the key's creator has the level `none`, for example `projects:full`, `maintenance:full` or `time_tracking:view`. Before, it named only the permission, for example `projects`, which the docs explain as any level other than `none`; a key raised to `own` still got 403.
    • The error code `forbidden` and `message` stay the same. Requests that need any level of a permission still name only the permission.
  26. Payment type names corrections booked through the API
    v1
    • Docs: The description of the payment field `type` now says that a `correction` is booked in craftivo or with POST /api/v1/invoices/{invoiceId}/payments/corrections. This applies to GET /api/v1/invoices/{invoiceId}/payments, to the responses of POST /api/v1/invoices/{invoiceId}/payments and POST /api/v1/invoices/{invoiceId}/payments/corrections, and to `payments` of documents in webhook deliveries and `payment` in `payment.created`. Values and shape stay the same.
  27. Form submission list pages through the own organization only
    v1
    • Fix: GET /api/v1/form-instance-submissions now reads only submissions of the organization in `X-Organization-Id`. Before, it paged through the submissions of all organizations and dropped foreign rows afterwards, so a page could come back short or empty while more own submissions followed.
    • Submissions the key cannot see through the project visibility rule no longer shorten a page. A page holds `limit` rows unless the list ends; a very large number of hidden rows can still end a page early, so keep following `meta.cursor` until `meta.isDone` is true.
    • The order stays the same: newest submission first.
    • A cursor from before this change, or any unreadable cursor, now returns 400 `invalid_filter` instead of 500 `internal_error`. Start again without `cursor`.
  28. Customer requests follow the visibility rule of the app
    v1
    • Fix: GET /api/v1/customer-requests and GET /api/v1/customer-requests/{customerRequestId} now show a key only the requests its creator sees in the app: all with the `projects:full` permission, otherwise those on a project the creator works on or with a task or appointment the office assigned to the creator, none without the `projects` permission. A linked or converted project that is still active must also be one GET /api/v1/projects/{projectId} shows to the key: on the project's team and not office-only. Before, every key read every request of the organization.
    • A request the key cannot see is left out of the list without losing a row while paging, and answers 404 `not_found` like an unknown id. This also applies to PATCH /api/v1/customer-requests/{customerRequestId}, which answered 403 before, and to `customerRequestId` in POST /api/v1/tasks.
    • Fix: POST /api/v1/form-instances with `contextType: customer_request` follows the same rule, also for a request without a project. Any other request answers 404 `form_context_not_found`. A repeated request with the same `Idempotency-Key` follows the same rule.
    • Fix: POST /api/v1/customer-requests/{customerRequestId}/files and POST /api/v1/customer-requests/{customerRequestId}/offers follow the same rule. A request the key cannot see answers 404 `not_found` and stores nothing; for files this comes before the file id is checked, which answered 400 `invalid_file` before. A repeated file request with the same `Idempotency-Key` follows the same rule.
    • Fix: The webhooks `customer_request.created`, `customer_request.updated`, `customer_request.converted` and `customer_request.archived` reach only endpoints whose creator can read the request through GET.
  29. Time entries and project members follow the project visibility rule
    v1
    • Fix: POST /api/v1/time-entries now books only on a project the key's creator can work on, as in the app: on the project's team, scheduled on it in the planning board, or with the full projects permission. Any other project, also through `orderId`, returns 404 `not_found` like an unknown id. Before, the entry was created with 201, and a completed project returned 400. An `orderId` whose project was deleted now returns the message `Order not found.`, and one without a project returns 404 instead of 400 when the key's creator lacks the full projects permission; this also applies to PATCH /api/v1/time-entries/{entryId}.
    • Fix: POST /api/v1/projects/{projectId}/members on a project the key's creator cannot see (no team row and no full projects permission) returns 404 `not_found` before the project management check. Before, it returned 403 `forbidden` or 500 `internal_error`.
    • A repeated request with the same `Idempotency-Key` follows the same rules.
  30. Changing a project or its members without project management rights returns 403
    v1
    • Fix: PATCH /api/v1/projects/{projectId}, PUT /api/v1/projects/external/{externalId} on an existing project and POST /api/v1/projects/{projectId}/members now return 403 `forbidden` with `details.requiredPermission` set to `projects:full` when the key's creator neither leads the project nor has the full projects permission. Before, the first request returned 500 `internal_error`; a repeated request with the same `Idempotency-Key` already returned 403.
    • Nothing is changed. PATCH and PUT on a project the key's creator cannot see still return 404 `not_found`.
  31. Creating and archiving a customer request needs the full projects permission
    v1
    • Creating and archiving a customer request now requires the full projects permission, as the craftivo app does. POST /api/v1/customer-requests and DELETE /api/v1/customer-requests/{customerRequestId} check `projects:full`. Before, any projects level was enough, also "own projects". Changing a request already needs `projects:full` since 2026-11-02.
    • A key whose creator has a lower level now gets 403 `forbidden` with `details.requiredPermission` set to `projects:full`, without a transition period. Nothing is created or archived. A repeated POST with the same `Idempotency-Key` follows the same rule and returns no stored response. Reading customer requests does not change. `GET /api/v1/me` lists the current permissions of the key's creator.
  32. Documents: amounts with the sales prices permission, payment data only with financial rights
    v1
    • Fix: GET /api/v1/quotes, /cost-estimates, /order-confirmations, /delivery-notes and /invoices and their detail routes now show `netAmountCents`, `taxAmountCents`, `grossAmountCents`, `payableAmountCents`, `deductions`, the prices of every position and `pdfUrl` also when the key's creator has the `salesPrices` permission ("see sales prices") without `financials`, as craftivo shows them. Before, GET returned them only with `financials`, while POST and PATCH responses already showed them with `salesPrices`.
    • Payment data stays with the `financials` permission: without it, `paidAmountCents`, `openAmountCents`, the amounts of every entry in `payments` and `settlement` on a final invoice are `null`, also in POST and PATCH responses and in replays with the same `Idempotency-Key`. POST, PATCH and GET now show the same fields.
    • Webhooks follow the same rule: `quote.*`, `cost_estimate.*`, `order_confirmation.*`, `delivery_note.*` and `invoice.*` carry the totals and `pdfUrl` when the webhook's creator has `financials` or `salesPrices`; payment data and `payment.created` amounts only with `financials`.
    • Members without either permission see no change. The standard roles are not affected; `salesPrices` is only active when granted to a single member.
  33. Webhooks: document amounts only with financial rights
    v1
    • Fix: `quote.*`, `cost_estimate.*`, `order_confirmation.*`, `delivery_note.*` and `invoice.*` carry amounts only when the webhook's creator may see them with GET, for example GET /api/v1/invoices/{invoiceId}: with the `financials` permission. Without it, `netAmountCents`, `taxAmountCents`, `grossAmountCents`, `paidAmountCents`, `openAmountCents`, `payableAmountCents`, `deductions`, the amounts of every entry in `payments` and `pdfUrl` are `null`, as on GET. The archived PDF shows all amounts, so its link is withheld too. Before, every webhook got the amounts.
    • Fix: `payment.created` follows the same rule: without the `financials` permission of the webhook's creator, `payment.amountCents`, `payment.skontoAppliedCents`, `payment.skontoPercentApplied`, `invoice.paidAmountCents` and `invoice.openAmountCents` are `null`. The payment itself, its date, method and reference stay.
    • The permission is checked at delivery: a retry after the permission was removed goes out without amounts.
    • Existing webhooks of members without the `financials` permission, for example technicians and site managers, now get `null` for these fields. The schemas in the specification allow `null` there.
  34. Webhooks: only documents the webhook's creator may read
    v1
    • Fix: `quote.*`, `cost_estimate.*`, `order_confirmation.*`, `delivery_note.*` and `invoice.*` reach a webhook only when its creator may read the document with its GET route, for example GET /api/v1/invoices/{invoiceId}: with the `projects:full` permission every document, otherwise only one on a project with a team row and never on a project marked office only. A document without a project, or on a deleted project, needs `projects:full`. Being scheduled on the project is not enough. Before, every webhook of the organization got every document event.
    • Fix: `payment.created` follows the same rule for its invoice.
    • Other webhooks get neither the event nor an entry in their delivery log. A webhook whose creator left the organization gets no document or payment events. A delivery already waiting for a retry is dropped when its creator may no longer read the document, also when the document has moved to another project since the event or was deleted.
    • Existing webhooks of members without the `projects:full` permission, for example technicians, get fewer events.
  35. Documents: only projects the key may read
    v1
    • Fix: GET /api/v1/quotes, /cost-estimates, /order-confirmations, /delivery-notes and /invoices and their detail routes show a document only when GET /api/v1/projects/{projectId} shows its project to the key: with the `projects:full` permission every project, otherwise only with a team row and never a project marked office only. A document without a project, or on a deleted project, needs `projects:full`. Before, every document of the organization was readable, only the amounts were hidden.
    • Any other document answers 404 `not_found` like an unknown one and is missing from the lists, also with the `projectId` or `customerId` filter. A page can now hold fewer rows than `limit`, or none, while `meta.isDone` is false. Keep following `meta.cursor` until `meta.isDone` is true; no row is skipped.
    • Being scheduled on a project no longer opens its documents for GET /api/v1/invoices/{invoiceId}/payments, finalize, send, accept, reject, draft changes, POST /api/v1/quotes and POST /api/v1/invoices. They answer 404 like for any other project the key may not read. POST /api/v1/customer-requests/{customerRequestId}/offers now also answers 404 `not_found` instead of 403 when the key may not read the linked project. A repeated call with the same `Idempotency-Key` follows the same rule, also when the stored quote has since moved to such a project.
    • `settlement` on a final invoice is now `null` only without financial rights; a key that may read the invoice may read its project.
  36. Form lists: only forms on projects the key may read
    v1
    • Fix: GET /api/v1/form-instances and GET /api/v1/form-instance-submissions list a form only when GET /api/v1/projects/{projectId} shows its project to the key. For a customer request context, the project the request is linked to or was converted into counts. Forms without a project stay visible. Before, every form of the organization was listed.
    • Forms on a deleted project no longer appear in either list, for every key.
    • A page can now hold fewer rows than `limit`, or none, while `meta.isDone` is false. Keep following `meta.cursor` until `meta.isDone` is true; no row is skipped.
  37. Form instances: only projects the key may read
    v1
    • Fix: POST /api/v1/form-instances accepts a `project` context only for a project that GET /api/v1/projects/{projectId} shows to the key: with the `projects:full` permission every project, otherwise only with a team row and never a project marked office only. Being scheduled on the project is not enough. A `customer_request` context follows the same rule for the project the request is linked to or was converted into. Before, any active project of the organization was accepted.
    • Any other project answers as an unknown context: 404 `form_context_not_found`. No form instance is created.
    • Fix: POST /api/v1/form-instance-submissions answers 404 `form_instance_not_found`, as for an unknown instance, when the instance belongs to such a project, directly or through its customer request, or to a deleted project. The instance stays open.
    • A repeated call with the same `Idempotency-Key` follows the same rule: when the key may no longer read the project, or the project was deleted, it answers 404 instead of the stored response.
  38. Webhooks: only projects the webhook's creator may read
    v1
    • Fix: `project.created` and `project.updated` reach a webhook only when its creator may read the project with GET /api/v1/projects/{projectId}: with the `projects:full` permission every project, otherwise only with a team row and never a project marked office only. Being scheduled on the project is not enough. Before, only office-only projects were held back. Other webhooks get neither the event nor an entry in their delivery log.
    • Fix: `task.created`, `task.updated` and `task.completed` reach a webhook only when its creator may see the task with GET /api/v1/tasks/{taskId}, including the project rule above. Before, every webhook got every task.
    • Fix: `form_instance.created` and `form_instance.submitted` for a form instance on a project follow the same project rule. A form instance on a customer request follows the project the request is linked to or was converted into, as GET /api/v1/form-instances does. Before, every webhook got them.
    • Fix: `time_entry.*` and `schedule_entry.*` carry only the `id` of the entry when the webhook's creator may not read its project with GET /api/v1/projects/{projectId}.
    • A webhook whose creator left the organization gets no project, task or form instance events.
    • Existing webhooks of members without the `projects:full` permission, for example technicians, get fewer events. A delivery already waiting for a retry is dropped when its creator may no longer read the project.
  39. Tasks and project upsert: only projects the key may read
    v1
    • Fix: POST /api/v1/tasks and PATCH /api/v1/tasks/{taskId} with `projectId` or `projectExternalId`, and the `projectId` filter of GET /api/v1/tasks, accept only projects that GET /api/v1/projects/{projectId} shows to the key: without the `projects:full` permission, only with a team row and never for a project marked office only. Being scheduled on the project is not enough. Before, any active project of the organization was accepted.
    • Any other project now answers as an unknown project: 404 `not_found` when creating or changing a task, an empty list for the filter. Existing integrations with such keys can no longer link tasks to these projects.
    • Fix: DELETE /api/v1/tasks/{taskId} deletes only tasks that GET /api/v1/tasks/{taskId} shows to the key; any other task answers 404 `not_found` and stays.
    • Fix: PUT /api/v1/projects/external/{externalId} answers 404 `not_found` when the `externalId` belongs to a project the key may not read, or to a deleted project. Nothing is changed or created. Before, the call failed with a different error and revealed that the project exists.
    • A repeated call with the same `Idempotency-Key` follows the same rule: when the key may no longer read the project, it answers 404 instead of the stored response.
  40. Tasks: only tasks the key may see
    v1
    • Fix: GET /api/v1/tasks, GET /api/v1/tasks/{taskId} and PATCH /api/v1/tasks/{taskId} now show and change only tasks that the key's creator may open in craftivo. Before, they checked only the organization.
    • For a task on a project, the key's creator 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. Being assigned to the task or scheduled on the project is not enough. The comment and file routes of a task follow the same rule.
    • Any other task answers 404 `not_found`. GET /api/v1/tasks leaves it out; a page can then hold fewer than `limit` tasks, or none, while `meta.isDone` is `false`. Follow `meta.cursor`. Existing integrations with such keys see fewer tasks.
  41. Retentions: close a retention the customer keeps
    v1
    • New: POST /api/v1/retentions/{retentionId}/forfeit with a required `note` closes a retention the customer keeps for good, for example because of defects. craftivo books no tax correction. POST /api/v1/retentions/{retentionId}/forfeit/revoke takes it back.
    • New state `forfeited` and new field `forfeitNote` on retentions.
  42. Invoices: deductions at the end and the payable amount
    v1
    • New field `payableAmountCents` on invoices: what the customer pays after deducted partial invoices and retentions. `grossAmountCents` does not change through a retention.
    • New field `deductions` on invoices: retentions, site power and water, construction insurance, each with its printed text. Deductions with `beforeTax` already reduce net, tax and gross.
    • New: GET /api/v1/retentions, POST /api/v1/retentions/{retentionId}/release and POST /api/v1/retentions/{retentionId}/revoke. A retention is not open until it is released.
  43. Final invoices: partial invoices on the quote of the order
    v1
    • `POST /api/v1/invoices` with `invoiceType: final` and `offsetScope: source` now deducts and replaces partial invoices on the quote of the order confirmation, if that quote has no other active order confirmation. With one order confirmation in the project, such a partial invoice no longer switches the scope to `project`.
    • `POST /api/v1/invoices/{invoiceId}/finalize` now answers 409 `conflict` for a final invoice with `offsetScope: source` if a partial invoice with a payment sits on a quote with several order confirmations. Create the final invoice with `offsetScope: project`. Finalized invoices keep their amounts.
  44. Final invoices: a partial invoice with a payment must be deducted
    v1
    • `POST /api/v1/invoices/{invoiceId}/finalize` now answers 409 `conflict` for a final invoice that leaves out a partial invoice with a payment it replaces. Before, the final invoice claimed the full amount on top of the payment. Finalized invoices keep their amounts.
  45. Final invoices: partly paid partial invoices count
    v1
    • A final invoice now deducts the amount paid on a partly paid partial invoice, at most its gross amount. `settlement` on `POST /api/v1/invoices`, `GET /api/v1/invoices/{invoiceId}` and finalize shows this with the new `paymentStatus` value `partially_paid`. Before, the deduction was 0.
    • New field `appliedByTaxRate` on each entry of `settlement.offsets`: the deduction per tax rate of the partial invoice. Finalized final invoices keep their amounts.
  46. Delete tasks, read and write comments, attach files
    v1
    • New: DELETE /api/v1/tasks/{taskId} deletes a task like in craftivo. Comments and attachments stay stored. Only the creator or a member with `projects:full` may delete; a repeated call answers 404.
    • New: GET and POST /api/v1/tasks/{taskId}/comments read and write the comments of a task. A new comment notifies the people involved.
    • New: POST /api/v1/tasks/{taskId}/files attaches a file id from POST /api/v1/files. At most 10 attachments per task (409 `attachment_limit_reached`) and 10 MB per document (413 `file_too_large`).
    • New: GET /api/v1/tasks/{taskId} returns `attachments` with download links. The field is missing, and the new comment and file routes answer 404, when the key's creator may not open the task in craftivo.
    • `Idempotency-Key` on the new POST routes first checks access, then answers with the existing record of the same API key.
  47. Finalize: an incomplete service period becomes the service date
    v1
    • `POST /api/v1/invoices/{invoiceId}/finalize`: An invoice with only the start or only the end of a service period gets that date as its service date, not the invoice date. `serviceDate` holds it, and `warnings` holds the new code `service_date_from_partial_period`.
    • `service_date_defaulted` now only appears if the invoice had no date at all.
  48. `POST /invoices`: error codes in the reference
    v1
    • The reference for `POST /api/v1/invoices` now names 409 `final_invoice_conflict` and 422 `idempotency_key_reused`. The API already answers with both.
  49. Final invoices: `settlement` follows project visibility
    v1
    • `settlement` on `GET /api/v1/invoices/{invoiceId}` is `null` if the key cannot read the project with `GET /api/v1/projects/{projectId}`. Without full project rights, the key's creator needs a team row on the project; being scheduled is not enough.
  50. Final invoices: open partial invoice drafts of the project
    v1
    • If the project has exactly one order, `POST /api/v1/invoices` with `invoiceType` `final` answers 409 `final_invoice_conflict` for every open partial invoice draft of the project, also for a draft without this order as its source.
  51. Accept: `pdfUrl` follows the amounts
    v1
    • `pdfUrl` in the response of `POST /api/v1/quotes/{quoteId}/accept` and `POST /api/v1/cost-estimates/{costEstimateId}/accept`, and in the other write routes that return a document, is `null` without the `salesPrices` permission of the key's creator, also on a replay. The archived PDF shows all amounts.
  52. Document lists: filters only within the organization
    v1
    • `customerId` and `projectId` on `GET /api/v1/quotes`, `/cost-estimates`, `/order-confirmations`, `/delivery-notes` and `/invoices` only match within the organization of the API key. Any other ID answers like an unknown ID: an empty page with `isDone: true`.
  53. Create final invoices through the API
    v1
    • `POST /api/v1/invoices` accepts `invoiceType` `final`. The result is a draft without a number with all lines and totals of the source; finalize it with `POST /api/v1/invoices/{invoiceId}/finalize`.
    • New field `settlement` on final invoices in `POST /api/v1/invoices`, `GET /api/v1/invoices/{invoiceId}` and finalize: the partial invoices with net, tax and gross, the amount deducted, and the remaining `payableCents`. As in craftivo only paid partial invoices reduce the amount. `null` without financial rights or access to the project.
    • New optional object `finalInvoice` with `offsetScope` for projects with more than one order.
    • A lock answers 409 `final_invoice_conflict`, for example if an active final invoice already covers the order or a partial invoice of the order is still a draft. `message` gives the reason in German.
  54. Create partial invoices through the API
    v1
    • `POST /api/v1/invoices` accepts `invoiceType` `partial` with the new object `partialInvoice`. It has three modes as in craftivo: `positions` (positions and quantities of the source), `lump_sum` (`percent` or `netCents` of the order total) and `free_amount` (`netCents` for a project without an order, with the new `source.type` `project`). The result is a draft without a number; finalize it with `POST /api/v1/invoices/{invoiceId}/finalize`.
    • `invoiceType` `down_payment` means the same as `partial`: the invoice is stored and returned with type `partial`.
    • A lock of the order answers 409 `partial_invoice_conflict`, for example partial invoices above 100 % of the order total. `message` gives the reason in German.
    • The same `Idempotency-Key` and source with other partial invoice values or another `invoiceType` answers 422 `idempotency_key_reused`.
  55. Correct a payment through the API
    v1
    • New: `POST /api/v1/invoices/{invoiceId}/payments/corrections` books a correction as a new line with a negative amount and a required `note`. The original payment stays unchanged and the invoice status is calculated again. A correction beyond the paid amount answers 409 `settlement_negative`.
    • New extra right `payments:correct`, enabled per key by the organization owner. `payments:write` alone is not enough. `GET /api/v1/me` lists it in `extraRights`.
    • `payment.created` also fires for these corrections. Evaluate `payment.type`: a `correction` has a negative `amountCents`.
  56. Finalizing an invoice always sets a service time
    v1
    • `POST /api/v1/invoices/{invoiceId}/finalize`: an invoice with neither a service date nor a service period now gets the invoice date as its service date, stored together with the number. The answer stays 200.
    • The response has two new fields: `warnings` holds `service_date_defaulted` in that case, `serviceDate` holds the service date, or `null` for a service period. The other document types answer as before.
  57. The webhook time_entry.created follows the time entry permission
    v1
    • Fix: `time_entry.created` now applies the same rule as `time_entry.updated` and `time_entry.deleted`. An endpoint whose creator may not see the entry, or who left the company, only gets `timeEntry.id`. Before, every endpoint got the whole entry. A delivery that was still waiting is sent the same way.
    • Endpoints whose creator may see all time entries see no change. Handle a `timeEntry` that holds only `id`. `GET /api/v1/me` lists the current permissions of the key's creator.
  58. Project webhooks follow the office-only rule
    v1
    • `project.created` and `project.updated` for a project marked office-only now only go to webhooks whose creator has the full projects permission, the same rule as `GET /api/v1/projects`. Other webhooks get no event and no entry in their delivery log. A delivery that was still waiting when the mark was set is dropped the same way.
  59. Changing a customer request needs the full projects permission
    v1
    • Changing a customer request now requires the full projects permission. PATCH /api/v1/customer-requests/{customerRequestId} checks `projects:full` for every field, as the craftivo app does. Before, this applied only to `nextStep`, `followUpDueAt`, `temperature` and `assignedUserId`.
    • A key whose creator has a lower level now gets 403 `forbidden` with `details.requiredPermission` set to `projects:full`, without a transition period. Reading customer requests does not change. `GET /api/v1/me` lists the current permissions of the key's creator.
  60. Single project read follows the project list rule
    v1
    • Fix: GET /api/v1/projects/{projectId} now shows the same projects as GET /api/v1/projects. Without the `projects:full` permission, the key's creator must be on the project's team. Being scheduled on the project is not enough. Any other project answers 404 `not_found`.
    • PATCH /api/v1/projects/{projectId} answers 404 `not_found` for these projects too. A replay of POST /api/v1/projects or a create through PUT /api/v1/projects/external/{externalId} answers 404 when the project is no longer visible. Keys with `projects:full` see no change.
  61. Customer request and document lists no longer fail on filtered pages
    v1
    • Fix: GET /api/v1/customer-requests, /quotes, /cost-estimates, /order-confirmations, /delivery-notes and /invoices answered 500 when the first rows read did not fill the page, for example archived requests before active ones, or quotes before invoices. They now answer 200.
    • A page can now be shorter than `limit`, or empty, while `meta.isDone` is `false`. Keep following `meta.cursor` until `meta.isDone` is `true`. Do not stop on a short or empty page.
  62. Document amounts need the financials permission
    v1
    • Fix: GET on quotes, cost estimates, order confirmations, delivery notes and invoices, in list and detail, shows amounts only when the key's creator has the `financials` permission. Without it, `netAmountCents`, `taxAmountCents`, `grossAmountCents`, `paidAmountCents`, `openAmountCents`, and the position fields `unitPriceCents`, `netAmountCents` and `subtotalCents` are `null`. `pdfUrl` is `null` too, because the PDF shows all amounts. The document stays readable, with title, status, recipient and position texts.
    • Fix: The payments in a document stay readable without the `financials` permission, but `amountCents`, `skontoAppliedCents` and `skontoPercentApplied` are `null`. GET /api/v1/invoices/{invoiceId}/payments still needs the permission and answers 403 without it. Webhook payloads do not change.
    • Existing connections keep their amounts while the key's creator has the `financials` permission. Only the owner can create keys, and the owner has this permission. A creator who lost it gets `null` from now on, without a transition period. Handle `null` in these fields.
  63. Idempotency-Key checks permissions on all write routes
    v1
    • Fix: Every `POST` and `PUT` route now checks the current permissions before it replays a stored response. A key whose creator lost the permission gets 403, also with an `Idempotency-Key` it used before. This covers among others POST /api/v1/properties, /parties, /customers, /projects, /quotes, /tasks and /time-entries.
    • A replay of POST /api/v1/quotes and POST /api/v1/customer-requests/{customerRequestId}/offers without the sales price permission shows the amounts as `null`. A replay of a project or customer request without the financials permission leaves out `estimatedBudget`, `actualCost` and `estimatedValue`. A project the key can no longer see answers 404.
  64. Idempotency-Key applies per API key
    v1
    • Fix: A stored response now belongs to the API key that created it. Another API key of the organization with the same `Idempotency-Key` no longer gets that response, for example with amounts it may not see. Its request runs as a new request under its own permissions.
    • Retry with the same API key. A retry through another API key can create a second record.
  65. Idempotency-Key checks permissions on schedule and maintenance writes
    v1
    • Fix: POST /api/v1/schedule-entries, POST /api/v1/maintenance-contracts/{contractId}/schedule-visits and POST /api/v1/maintenance-visits/{visitId}/complete no longer replay a stored response before the permission check. A key without the permission gets 403, the response comes from the current record under the current read permissions.
    • A replay of POST /api/v1/schedule-entries and schedule-visits carries no `meta.conflicts`, and a record deleted in the meantime answers 404. A repeated `complete` answers with `alreadyCompleted: true`.
  66. Request, decide, change and delete absences
    v1
    • New: POST /api/v1/absences requests an absence for the key's creator. Entering one for another member needs the `time_tracking` full permission and is approved at once, like in the app. An overlap answers 409 `absence_overlap`; send `allowOverlap: true` to save anyway.
    • New: POST /api/v1/absences/{absenceId}/approve and /reject need the `time_tracking` full permission. The same decision again changes nothing and sends nothing.
    • New: PATCH and DELETE /api/v1/absences/{absenceId} follow the rule of the app: the member while it is pending, members with the `time_tracking` full permission always. Otherwise 403 or 409 `absence_not_pending`.
    • Sick leave stays `absent` in every response, also in a conflict, without the employees permission at level `full`. Notes and rejection reasons are write-only. `Idempotency-Key` never replays a stored answer: Permission and visibility apply to every call.
  67. Delete and restore properties, archive parties
    v1
    • New: DELETE /api/v1/properties/{propertyId} marks a property as deleted. Its projects, documents and parties stay. A repeated call answers 410 `property_deleted`.
    • New: POST /api/v1/properties/{propertyId}/restore brings a deleted property back. On an active property it changes nothing.
    • Both need the permission `projects:full`, like the delete button in craftivo. Otherwise they answer 403 `forbidden`.
    • New: DELETE /api/v1/parties/{partyId} ends a party, like `PATCH` with `isActive: false`. PATCH with `isActive: true` brings it back. Default parties of the property keep pointing at it. `POST /api/v1/quotes` and `PATCH` with a property recipient answer 400 until the default is removed or `recipientMode` changes; finalized documents do not change.
    • New webhook events: `property.deleted` and `property.restored`, sent for craftivo and the API.
  68. Create and change entries of the position library
    v1
    • New: POST /api/v1/positions creates an entry. It requires `title`, `unit`, `quantity` and `unitPriceCents`, as the dialog in craftivo does.
    • New: PATCH /api/v1/positions/{positionId} changes an entry. Send only what changes; `null` removes an optional field. Linked offer templates follow.
    • `costPriceCents` writes the purchase price with up to three decimals and needs the permission `financials`. It stays write-only: `Position` never returns purchase prices or calculations.
    • An entry with a price calculation answers 409 `position_price_calculated` to `unitPriceCents` and `quantity`. A wholesaler article answers 409 `position_ids_price` to `costPriceCents`. Entries cannot be deleted or archived through the API.
  69. Create, change and archive maintenance contracts
    v1
    • New: POST /api/v1/maintenance-contracts creates a contract with its number, equipment and first open visit. It requires the same fields as the dialog in craftivo. With `templateId` missing values come from the maintenance type.
    • New: PATCH /api/v1/maintenance-contracts/{contractId} changes a contract. Send only what changes; `null` clears an optional field. An open visit moves to the new due month.
    • New: POST /api/v1/maintenance-contracts/{contractId}/status sets `aktiv`, `pausiert` or `beendet`. POST /api/v1/maintenance-contracts/{contractId}/archive and /restore move a contract to the archive and back. An archived contract answers 409 `contract_archived` to changes.
    • New: GET /api/v1/maintenance-templates lists the maintenance types with the maintenance permission `full`. `MaintenanceContract` now carries `templateId` and `templateName`.
    • New webhook events `maintenance_contract.created`, `.updated`, `.archived` and `.restored`, for changes in craftivo and through this API. Prices only reach an endpoint whose creator has the maintenance permission `full`.
  70. Change and delete time entries
    v1
    • New: PATCH /api/v1/time-entries/{entryId} changes times, break, category, project and note. Send only what changes. Every change lands in the entry's history with the key as actor.
    • New: DELETE /api/v1/time-entries/{entryId} deletes a draft. Other statuses answer 409 `time_entry_not_draft`, because only approved time reaches the payroll export.
    • A time entry locked by an invoice or a closed period answers 409 `time_entry_locked` for both, also for the owner. A running timer answers 409 `time_entry_running`. A submitted entry answers 409 `time_entry_submitted` unless the key's creator is the owner.
    • New webhook events `time_entry.updated` and `time_entry.deleted`, for edits in craftivo and through this API. An endpoint whose creator may not see the entry only gets `timeEntry.id`.
  71. Schedule and complete maintenance visits
    v1
    • New: POST /api/v1/maintenance-contracts/{contractId}/schedule-visits puts the next open visit of a contract on the planning board. A double booking or an absence answers 409 `schedule_conflict`; `force: true` schedules anyway.
    • New: POST /api/v1/maintenance-visits/{visitId}/complete completes a visit, optionally with `completedAt` and a technician note, and creates the next open visit. Completing it again answers 200 with `alreadyCompleted: true`. Photos are not part of the API.
    • New webhook events `maintenance_visit.scheduled` and `maintenance_visit.completed`, for changes in craftivo and through this API.
    • `schedule_entry.created` and `schedule_entry.updated` now also fire for scheduled and completed maintenance visits.
  72. Write assignments on the planning board
    v1
    • New: POST /api/v1/schedule-entries plans members or a team on a project for one or several days, with notes and the status `planned` or `confirmed`.
    • New: PATCH /api/v1/schedule-entries/{entryId} moves an assignment and changes members, team, notes or the status `planned`, `confirmed`, `cancelled`. New: DELETE /api/v1/schedule-entries/{entryId} deletes an assignment or a single date of a recurring assignment.
    • A double booking or an absence answers 409 `schedule_conflict` with the assignments in `details.conflicts` and writes nothing. `force: true` plans anyway, like "Trotzdem einplanen" in craftivo. Recurring assignments, pre-planning and project bars answer 409 `use_web_planner`.
    • New webhook events `schedule_entry.created`, `schedule_entry.updated` and `schedule_entry.deleted`, for changes in the planning board and through this API.
  73. Practice accounts: sample data and reset
    v1
    • A new practice account (Übungskonto) starts with sample data: customers with umlauts such as „Müller & Söhne GbR“ (external ids `beispiel-…`), a multi-family house with its property management as a party, customer requests in several board columns, quotes, an invoice with a partial payment, a crew with time entries, and planning board assignments.
    • New: POST /api/v1/sandbox/reset deletes all data of the practice account for good and creates the sample data again. It answers 202 with `status: running`; GET /api/v1/sandbox/reset shows the state until `status` is `completed`. Members, API keys with their quota, and webhook endpoints stay. Only a read-write key of the owner may reset; any other organization answers 403 `not_a_sandbox` and keeps its data. A second reset while one runs answers 409 `reset_in_progress`; a retry with the same `Idempotency-Key` answers with the run it started.
  74. Quota package: +10,000 requests until the end of the month
    v1
    • The owner can book a one-time quota package in craftivo under Settings > API & Webhooks. Each package raises the organization's daily quota by 10,000 requests from payment until the end of the calendar month (Europe/Berlin); several packages add up. `RateLimit-Limit` and `RateLimit-Remaining` show the raised quota at once. After a full refund the package no longer counts. Practice accounts cannot book it.
  75. API add-on for Starter and Premium
    v1
    • Starter and Premium organizations with the API add-on have API access. Their quota is 5,000 requests per day and organization and 60 per minute and key; the `RateLimit-*` headers show this quota. Business organizations keep their quota.
    • The 403 `plan_feature_required` message for organizations without API access now names both ways: "API access requires the Business plan or the API add-on." Code and details are unchanged.
    • When a cancelled API add-on reaches the end of its paid period, reads keep working and writes return 402 `billing_access_required` with `details.feature: api_access`. Webhook events are no longer delivered; the delivery log shows them as canceled. Deleting a webhook keeps working.
  76. Practice accounts never deliver emails
    v1
    • A practice account (Übungskonto) never sends an email to a real recipient, also in craftivo's production environment. POST /api/v1/{document}/{id}/send then answers `sink: true`. The email is in the mailbox history with recipient, subject, time and the status "Testversand, nicht zugestellt". The document status and the webhook events change as after a real send. Webhooks to endpoints of the practice account itself are still delivered. Other organizations are not affected.
  77. Practice accounts: own daily quota and 1 GB storage
    v1
    • A practice account (Übungskonto) has all Business features including the API. Its quota is 2,000 requests per day and organization and 60 per minute and key; the `RateLimit-*` headers show this quota.
    • POST /api/v1/files returns 413 `storage_limit_reached` with `details.maxStorageBytes` when the 1 GB storage of a practice account is full. Nothing is stored. Other organizations are not affected.
  78. Paid invoices with incomplete payment records reject payments
    v1
    • POST /api/v1/invoices/{invoiceId}/payments now also returns 409 `payment_not_allowed` with `details.reason: paid_without_payment_record` when the invoice is marked as paid in craftivo and has payments, but they do not cover the amount. Before, the reason only applied to paid invoices without any payment. Nothing is open on such an invoice, so nothing is booked and its status stays paid. Invoices whose payments cover the amount behave as before.
  79. Maintenance invoices check the recipient party
    v1
    • POST /api/v1/invoices with `source.type: maintenance_visit` now returns 409 `visit_not_billable` with `details.reason: recipient_party_inactive` when the recipient party on the maintenance contract, for example the property management, has ended or does not belong to the contract's property. No order and no draft are created. Before, the draft was created with the old recipient's address and email. Choose a new recipient on the contract in craftivo and send the request again. "Rechnung erstellen" in craftivo follows the same rule.
    • A required custom field for orders no longer makes this request fail with 500. The order "Wartungsabrechnung <contract number>" that carries the draft is created without required fields, as in craftivo.
  80. Appending positions without sections works as before
    v1
    • PATCH /api/v1/quotes/{quoteId} and PATCH /api/v1/invoices/{invoiceId} check the section order of `appendPositions` only when the request sends section headers (`type: section`) or `sectionId`. Appending plain positions to a document with sections no longer returns 400 `invalid_body`.
    • Appended positions without `sectionId` are placed as in craftivo. If no position of the document has a `sectionId` yet, for example when it has only section headers or is an older document, they belong to the last section, and its subtotal and lump sum (Pauschalpreis) include them. Otherwise they stand outside the sections, as before.
  81. Delivery email on the document skips the delivery party check
    v1
    • PATCH /api/v1/quotes/{quoteId} and PATCH /api/v1/invoices/{invoiceId} no longer return 400 `invalid_body` because of an ended delivery party when the document has its own delivery email (`deliveryEmailOverride`). The document is sent to that email, so the delivery party on the document or the property is not used and not checked. POST /api/v1/quotes follows the same rule. If a request removes the delivery email, the delivery party is checked again and an ended one is rejected with 400 as before.
  82. Invoice drafts from maintenance visits
    v1
    • POST /api/v1/invoices accepts `source.type: maintenance_visit` with the id of a maintenance visit from GET /api/v1/maintenance-visits. It bills the visit as "Rechnung erstellen" in craftivo: a new order "Wartungsabrechnung <contract number>" carries a draft invoice without a number. The `Idempotency-Key` header is required as for the other sources.
    • If the visit is already billed and its invoice draft still exists, the response is 200 with that invoice and nothing is created. A new draft answers 201.
    • Billing a visit needs the maintenance contracts plan feature (otherwise 403 `plan_feature_required` with `details.feature`), the `maintenance:full`, `financials` and `create_invoice` permissions and the permission to create orders. A visit that cannot be billed yet answers 409 `visit_not_billable` with `details.reason`: `not_completed`, `advance_not_due`, `nothing_to_bill`, `customer_deleted` or `property_deleted`.
  83. Sections in quotes and draft invoices
    v1
    • POST /api/v1/quotes, PATCH /api/v1/quotes/{quoteId} and PATCH /api/v1/invoices/{invoiceId} accept section headers in `positions` and `appendPositions`: a row with `type: section`, a `title`, your own `id` and, for a sub-section, `parentSectionId`. Positions point to their section with `sectionId`. Rows that would end up in another section than sent are rejected with 400.
    • Document rows in GET, POST and PATCH responses have the new fields `sectionId`, `parentSectionId` and `subtotalCents`. `subtotalCents` is the section subtotal as shown in craftivo and is `null` when the key's creator cannot see sales prices.
    • Section headers carry no price: in POST /api/v1/quotes and PATCH /api/v1/quotes/{quoteId}, a request that only adds section headers needs no `financials` permission, as in craftivo. Rows with a price still need it. PATCH /api/v1/invoices/{invoiceId} always needs `financials`.
  84. POST /quotes with positions needs the financials permission
    v1
    • POST /api/v1/quotes with at least one row in `positions` now returns 403 `forbidden` when the key's creator lacks the `financials` permission, for example a site manager without price access, and creates nothing. This is the same rule as for `positions` and `appendPositions` in PATCH /api/v1/quotes/{quoteId}. A draft without positions can still be created without `financials`.
    • The response of POST /api/v1/quotes shows amounts like craftivo does for the key's creator: without the `financials` or `salesPrices` permission, the totals and position prices are `null`.
  85. project.updated for archiving, completing and converting
    v1
    • `project.updated` is now also sent when a project or order is archived or restored, completed, reopened for rework, moved to another order status on the board, converted from project to order or back, and when its pre-planning bar on the schedule sets new dates. Each of these sends exactly one event. Completing an order that is already completed sends nothing. Deleting, and moving projects because a status column is deleted, still send no event.
  86. GET /projects without full project access
    v1
    • GET /api/v1/projects returns 200 instead of 500 `internal_error` when the key's creator lacks the `projects:full` permission, for example a technician with `projects:own`. The list keeps showing only projects where they are on the team. A page can then hold fewer than `limit` projects, or none, while `meta.isDone` is `false`. Follow `meta.cursor`.
  87. Fixes after review: paging, visibility and recipient checks
    v1
    • GET /api/v1/maintenance-contracts and GET /api/v1/maintenance-visits page with a real cursor. One request reads at most 300 rows; with filters or search a page can hold fewer than `limit` rows, or none, while `meta.isDone` is `false`. Follow `meta.cursor`. Visits with the same `dueDate` are sorted by creation.
    • GET /api/v1/absences reads only the absences of `memberId` when it is set or when the API user may only see their own. Without `memberId`, a range that would read more than 4,000 absences returns 400 `invalid_filter` instead of 500.
    • GET /api/v1/schedule-entries/{entryId}: without the `schedule` permission a recurring assignment only spans the dates inside the app window, like the list.
    • PATCH /api/v1/quotes/{quoteId} and PATCH /api/v1/invoices/{invoiceId} with a property recipient check the delivery party chosen on the document, if any, instead of the property's default delivery party.
    • PATCH /api/v1/parties/{partyId} with `roleLabel` but without `role` returns 400 `invalid_body` when the stored role is not `sonstige`, instead of dropping the label.
    • `property.updated` is only sent when a field of the public property changes; changing only the contact person or the on-site contacts sends nothing.
    • The description of `project.updated` now lists the ways that send it. Archiving, restoring, completing, deleting and other status changes outside these ways send no event yet.
  88. PATCH checks a chosen recipient party
    v1
    • PATCH /api/v1/quotes/{quoteId} and PATCH /api/v1/invoices/{invoiceId} now check a recipient party chosen in craftivo (`recipientMode` `property_party`) on every call, also when only `title` is sent. If the property was deleted, the party was ended or moved to another property, or a default address or delivery party of the property was ended, the API returns 400 `invalid_body` and changes nothing, instead of switching the recipient silently. Send `recipientMode` to choose a new recipient.
  89. Role and permissions per organization in GET /me
    v1
    • GET /api/v1/me returns `permissions` for each organization: the effective permissions of the key's creator, as in the craftivo app. A yes/no permission appears as its key (`financials`), a graded permission as `key:level` (`projects:own`); permissions not granted and the level `none` are left out.
    • A 403 `forbidden` because a permission of the key's creator is missing now names it in `details.requiredPermission`, in the same notation, when the missing permission is known. The error code stays `forbidden`.
    • A permission error of an endpoint without its own mapping returns 403 `forbidden` instead of 500 `internal_error`.
  90. Read absences, company vacations and holidays
    v1
    • Added GET /api/v1/absences: absences of members that overlap the required range `from`…`to` (at most 366 days), with the filters `memberId`, `type` and `status`. Each absence has `memberId`, `type`, `status`, `startDate`, `endDate`, `isHalfDay` and optional `startTime`, `endTime` and `companyVacationId`; notes, rejection reasons and vacation balances are not part of the API.
    • Without the time tracking permission at level `view` the API user only sees their own absences; `memberId` of another member returns 403 `forbidden`. Without the employees permission at level `full`, sick leave has the type `absent` and `type=sick` returns 403 `forbidden`.
    • Added GET /api/v1/company-vacations and GET /api/v1/holidays for the same kind of range. Every member may read them; a recurring holiday returns one entry per year in the range.
  91. Read, create and change properties and their parties
    v1
    • Added GET and POST /api/v1/properties and GET and PATCH /api/v1/properties/{propertyId}: buildings (Objekte) with address, external id, notes and four default parties. Filters `customerId`, `partyId` and `query`.
    • GET /api/v1/properties/{propertyId} also returns the linked customers, all parties with their default roles and the linked projects and orders the key owner may see.
    • Added GET and POST /api/v1/parties and GET and PATCH /api/v1/parties/{partyId}: customers attached to a property with a role such as `hausverwaltung` or `weg`, delivery routing and `defaultFor` (`address`, `delivery`, `quote`, `invoice`). Filters `propertyId`, `customerId`, `role`, `query` and `includeInactive`.
    • Set the address party (Anschrift-Partei), the delivery party (Versand-Partei) and the default quote and invoice recipients with PATCH /api/v1/properties/{propertyId}; null removes one.
    • A property deleted in craftivo is not listed and answers 410 `property_deleted`, also for its parties and as `propertyId` of a project. Writing needs the projects permission; there is no delete.
    • Projects and orders now return `propertyId`. POST, PATCH and PUT on /api/v1/projects accept `propertyId` to link a property; on update, `propertyId: null` removes it.
    • New webhook events `property.created` and `property.updated`, also for properties created or changed in craftivo.
  92. Read maintenance contracts and visits
    v1
    • Added GET /api/v1/maintenance-contracts with filters `customerId`, `propertyId`, `status`, `query` and `includeArchived`; archived contracts are only listed with `includeArchived=true`.
    • Added GET /api/v1/maintenance-contracts/{contractId} with equipment and devices, interval, next due date and linked property.
    • Added GET /api/v1/maintenance-visits: visits whose target month lies in a required range `from`…`to` of at most 366 days, with filters `contractId`, `status` and `memberId`.
    • Added GET /api/v1/maintenance-visits/{visitId} with technician notes, protocol, recorded extras without prices and the invoice draft.
    • Reading requires the maintenance permission. Prices, billing mode, catalog positions and invoice drafts are only returned with the maintenance permission `full`.
  93. Read the planning board
    v1
    • Added GET /api/v1/schedule-entries: assignments, pre-planning bars and dates of recurring assignments in a required range `from`…`to` of at most 56 days, with filters `memberId`, `teamId`, `projectId`, `status` and `entryType`.
    • A recurring assignment returns one entry per date in the range, with the id of that date and `recurrenceId` of the series.
    • Added GET /api/v1/schedule-entries/{entryId} with members, crew, project, notes and status; for a series also `recurrenceRule`.
    • Members with the planning permission see the whole board including pre-planning. All other members only see their own released assignments in the window of their app, like in the mobile app.
  94. Read crews and their members
    v1
    • Added GET /api/v1/teams: all active crews of the organization, sorted by name, with `name`, optional `color`, `isActive`, `memberIds` and an optional `leaderId`.
    • Added GET /api/v1/teams/{teamId}. It also returns a deactivated crew with `isActive: false`; a crew of another organization returns 404 `not_found`.
    • `memberIds` and `leaderId` use the ids of `GET /api/v1/members` and only contain members that this list shows. `leaderId` is the member with the leader role who joined the crew first.
  95. Project webhooks in the public form, also for new projects from craftivo
    v1
    • Fix: `project.created` and `project.updated` now always carry the public project in `data.project`, as `GET /api/v1/projects/{projectId}` returns it, including `customerRequestId` and `visibleOnBoard`. Until now, changes made in craftivo sent the internal record. The specification already promised the public form; both events are now documented under webhooks.
    • `estimatedBudget` and `actualCost` are only included when the member who created the webhook may see financials. Otherwise both fields are left out.
    • `project.created` now fires once for every new project, whichever way it was created: also when a customer request is converted in craftivo, for a project created from a website form, and for the hidden draft project behind a quote for a customer request (`POST /api/v1/customer-requests/{customerRequestId}/offers`, with `visibleOnBoard: false`). Converting the request later sends no second `project.created`, but `project.updated` with the new state; a project then has `visibleOnBoard: true`. Projects imported from another program send nothing.
    • Both events are computed after the transaction and are delivered asynchronously, so a payload can already show later changes.
  96. Versioning rules and deprecation headers
    v1
    • v1 never breaks: no field, endpoint or enum value is removed or changes its type, and no request gets a new required field. Breaking changes get a new endpoint or `/api/v2`. Every contract change is listed on this page.
    • New optional fields, new endpoints and new enum values can appear in v1 at any time. Ignore unknown fields and handle unknown enum values.
    • Deprecated endpoints, parameters and fields are marked `deprecated: true` with `x-deprecated-at` and `x-sunset` in the specification. They keep working in v1; `x-sunset` lies at least six months after `x-deprecated-at` and is the earliest date a later version may drop them.
    • Every response of an endpoint that is deprecated or has a deprecated parameter or field carries the headers `Deprecation` (RFC 9745) and `Sunset` (RFC 8594). Nothing is deprecated today.
  97. Record and list invoice payments
    v1
    • Added POST /api/v1/invoices/{invoiceId}/payments. It records a payment as "Zahlung erfassen" in craftivo with `amountCents` (integer cents), `paidAt` (`YYYY-MM-DD`), `method` (`bank_transfer`, `cash`, `card`, `direct_debit`, `other`) and a required `externalId`, plus optional `skontoAppliedCents`, `skontoPercentApplied`, `reference` and `note`. The response (201) is the payment with the invoice state (`status`, `paidAmountCents`, `openAmountCents`) and `warnings`.
    • A payment is never booked twice: `externalId` is unique per invoice. The same `externalId` with the same data returns 200 with the existing payment; with different data 409 `external_id_conflict`. The `Idempotency-Key` header stays optional; on this route the same key with a different payment returns 422 `idempotency_key_reused`.
    • Overpayment returns 409 `payment_exceeds_open_amount`. A draft, a cancelled invoice, a credit note and other invoices that take no payments return 409 `payment_not_allowed` with `details.reason`. Skonto after the deadline, without skonto terms or above the agreed rate is booked with a warning.
    • Needs the extra right `payments:write` of the API key, and the `financials` permission and full project access of the key's creator. The history and the notification name the API key.
    • Added GET /api/v1/invoices/{invoiceId}/payments: all payments of an invoice, oldest first, without pagination; a credit note returns `[]`. Payments in documents now carry `externalId` and `createdAt`.
    • Added the webhook event `payment.created` with `{ payment, invoice }`, for payments and corrections from craftivo and the API. `invoice.partially_paid` and `invoice.paid` still fire only when the status changes, otherwise `invoice.updated`.
  98. Invoice draft from a quote or order confirmation
    v1
    • Added POST /api/v1/invoices with `source: { type: "quote" | "order_confirmation", id }`. It creates a standard invoice draft without a number (201) with the totals, positions, sections and measurements of the source, as "In Rechnung umwandeln" in craftivo. The response is the invoice in the detail format. Finalize it with POST /api/v1/invoices/{invoiceId}/finalize.
    • This is the only route with a required header: without `Idempotency-Key` it returns 400 `idempotency_key_required`. A retry with the same key returns the first invoice and creates no second one.
    • Only `invoiceType: standard` (or no `invoiceType`); anything else returns 400 `unsupported_invoice_type`. A source that is a draft, rejected, archived, cancelled or superseded returns 409 `invalid_source_status`; an addendum or a source in a collective invoice 409 `source_not_invoiceable`; an unknown, deleted or foreign source 404.
    • Needs a `read_write` key and the `financials` and `create_invoice` permissions of the key's creator. The history names the API key. No webhook fires for the draft; `invoice.created` fires on finalizing.
    • Added PATCH /api/v1/invoices/{invoiceId} for draft standard invoices with the same fields as PATCH /api/v1/quotes/{quoteId}. A finalized invoice returns 409 `document_finalized`, any other invoice type 409 `unsupported_invoice_type`. Position ids change with every save.
  99. Customer accepts or rejects, order confirmation from a quote
    v1
    • Added POST /api/v1/quotes/{quoteId}/accept and /reject, and POST /api/v1/cost-estimates/{costEstimateId}/accept and /reject. They record the customer's answer, as "Angenommen" and "Nicht angenommen" in craftivo. The time is always now. The response is the document in the detail format.
    • Accepting takes no request body (a body with `date`, `reason` or any other field returns 400) and works only from `sent` or `customer_review`: a draft returns 409 `document_not_finalized`, a finalized but unsent document 409 `document_not_sent`. It needs the `projects: full` permission of the key's creator.
    • Rejecting needs `rejectionReason` from the fixed list (`customer_chose_other`, `price_expectation_not_met`, `project_postponed`, `project_not_implemented`, `no_response`, `superseded_by_new_offer`, `other`); `rejectionComment` is optional. It works from `finalized`, `sent` or `customer_review` and needs the `financials` permission of the key's creator.
    • An accepted document cannot be rejected and vice versa: 409 `invalid_status_transition`. Repeating the same answer returns 200 without a second history entry or webhook.
    • Only the document changes. The customer request column and the project status stay as they are. The customer link with signature stays a second way to accept.
    • Webhooks fire as in craftivo: `{type}.updated` and `{type}.accepted` or `{type}.rejected`.
    • Added POST /api/v1/quotes/{quoteId}/order-confirmation. It creates an order confirmation draft without a number in the quote's project (201) and needs the `create_offer` permission of the key's creator. The quote need not be accepted; drafts, rejected and archived quotes return 409. A quote without customer returns 422 `customer_required`. Each call creates a new draft; a retry with the same `Idempotency-Key` returns the first one. No webhook fires for the draft.
  100. Email documents to the customer
    v1
    • Added POST /api/v1/{quotes|cost-estimates|order-confirmations|delivery-notes|invoices}/{id}/send. It emails the archived PDF (with XRechnung also the XML file) from the organization's sender mailbox, as "Per E-Mail versenden" in craftivo. Credit notes and down payment invoices are sent under /invoices.
    • The endpoints need the extra right `documents:send` of the API key and the `mailbox` and `financials` permissions of the key's creator.
    • Pass `templateId`, or `subject` and `body`; otherwise 422 `message_required`. craftivo never writes the text itself.
    • The recipient is the delivery address frozen at finalizing, e.g. the property management of the building; craftivo never falls back to the customer's email. `to`, `cc` and `bcc` replace it. The organization's default BCC is added for quotes, invoices and down payment invoices.
    • Only finalized documents are sent: a draft returns 409 `document_not_finalized`, a cancelled or superseded one 409 `document_not_sendable`. A missing PDF returns 409 `pdf_not_ready` with `Retry-After`.
    • `dryRun: true` returns the email without sending it and is never stored under the `Idempotency-Key`. A retry with the same key returns the first response; an unknown outcome returns 409 `send_outcome_unknown` and never sends twice. A document already sent needs `resend: true`.
    • The webhook `{type}.sent` fires only when the status changes from finalized to sent, not for a resend.
  101. PDF after finalizing
    v1
    • After POST /api/v1/{type}/{id}/finalize craftivo creates the archived PDF in the background, usually within seconds. GET on the document returns `pdfUrl` as soon as the PDF exists; until then `pdfUrl` is `null`.
    • For invoices, down payments and credit notes craftivo also creates the e-invoice when the customer needs one. Missing e-invoice details do not block the PDF.
    • The PDF is created once and never replaced. The document stays finalized even if creating the PDF fails; craftivo then creates it on the next download.
  102. Finalize documents
    v1
    • Added POST /api/v1/{quotes|cost-estimates|order-confirmations|delivery-notes|invoices}/{id}/finalize. It assigns the next number from the organization's number sequence and locks the document, as finalizing in craftivo. The response is the document in the detail format with `number`.
    • The endpoints need the extra right `documents:finalize` of the API key (otherwise 403 `insufficient_scope` with `details.requiredScope`) and the `financials` permission of the key's creator for every document type (otherwise 403 `forbidden`).
    • Missing information returns 422 `finalization_incomplete`. `details.missingFields` lists every finding as `{ code, field, message }`; the first one is the error craftivo shows in the app.
    • A document that is no longer a draft returns 409 `document_finalized`, one being edited in craftivo 409 `document_locked`. Send an `Idempotency-Key`: a retry after a timeout returns the first response with the same number.
    • Webhooks fire as for finalizing in craftivo (`{type}.created` and `{type}.finalized`). `pdfUrl` is `null` in the response; craftivo creates the PDF later.
  103. Idempotency keys per path, kept 30 days
    v1
    • An `Idempotency-Key` now applies per organization, method and path. Reusing a key on another path no longer replaces the stored response of the first path; the first stored response wins.
    • Stored responses are deleted 30 days after the first request. A request with the same key after that runs again.
  104. Convert customer requests
    v1
    • Added POST /api/v1/customer-requests/{customerRequestId}/convert with `target` `project` (default) or `order`. It marks the request as won like the conversion in craftivo; everything else comes from the request. The response is the project as returned by GET /api/v1/projects/{projectId}.
    • Without a linked project, a new project or order is created with the next number and the call returns 201 and fires `project.created`. A draft project from POST .../offers is taken over with 200 and fires no `project.created`; with `order` a new order number replaces its project number.
    • An already converted request returns 409 `already_converted` with `details.convertedProjectId` and changes nothing. With an Idempotency-Key the first status and body are replayed.
    • `target: null`, an empty `target` and any other field return 400 and convert nothing.
    • `customer_request.converted` fires once per conversion, also when the request was created just before and both changes are delivered together. The endpoint needs the projects permission at level full; a new project also needs `create_project`.
  105. Work customer requests like the pipeline
    v1
    • Added GET /api/v1/customer-request-columns. It lists the columns of the board on which new requests land, with `kind` `open`, `won` or `lost`. An organization without a board gets an empty list.
    • Added GET /api/v1/project-statuses with `name`, `order`, `systemKey` (`planning`, `completed`, `cancelled` or null) and `isSystem`. These are the ids `statusId` accepts for projects; orders use a separate list.
    • POST and PATCH /api/v1/customer-requests now accept `nextStep`, `followUpDueAt`, `temperature` and `assignedUserId`; null clears them. They need the projects permission at level full. A member of another organization returns 400. Setting `followUpDueAt` records no contact and creates no call task.
    • PATCH /api/v1/customer-requests now rejects a column of another board with 400, as POST already did. Moving to a `won` or `lost` column creates no project and sets no loss reason.
    • POST /api/v1/tasks accepts `customerRequestId`; the task then shows in the request. The customer and project must belong to the request, otherwise 400. Tasks now return `customerRequestId` and `location`, also in webhooks.
    • `location` is writable on appointments and measurements, other types return 400. Without `location`, an appointment or measurement on a request gets the request's site, never the customer's address.
  106. Change draft quotes
    v1
    • Added PATCH /api/v1/quotes/{quoteId}. It changes the header of a draft quote, replaces its positions (`positions`) or adds positions after the existing ones (`appendPositions`); both together return 400. Fields and checks are the same as for POST /api/v1/quotes.
    • Fields you do not send stay as saved, including texts, discounts, payment terms, PDF settings and the measurements (Aufmaß) of existing positions.
    • Position ids change with every PATCH; read them again from the response. Changing positions needs the `financials` permission of the key's creator.
    • Document details now include `version`. Send it with PATCH: if the quote was saved in the meantime, the request returns 409 `version_conflict`. Without `version` the last write wins.
    • A quote that is no longer a draft returns 409 `document_finalized`, one being edited by someone else in craftivo 409 `document_locked`. A draft change fires no webhook.
  107. Create draft quotes
    v1
    • Added POST /api/v1/quotes. It creates a draft quote without a number in a project; `projectId` is required and the customer comes from the project.
    • Positions come from the position library by `positionId` or `articleNumber` with a `quantity`. Title, description, unit, price and calculation are taken over as if the position had been picked in craftivo, so the totals match. Free positions take `title`, `unit`, `quantity` and `unitPriceCents`.
    • An `articleNumber` that matches more than one library position returns 409 `ambiguous_article_number`. The VAT rate comes from the organization; `vatRatePercent` per position is only accepted in Switzerland.
    • A draft fires no webhook; `quote.created` follows when the quote is finalized.
    • Document details now include `introductionText`, `conclusionText`, `recipientMode`, `deliveryEmail`, `deliveryEmailOverride` and `deliveryPartyId`; positions include `positionLibraryId`.
    • POST /api/v1/customer-requests/{customerRequestId}/offers now needs the `create_offer` permission, writes the history and the default texts, and rejects a customer without an address with 400.
  108. Position library
    v1
    • Added GET /api/v1/positions with full-text `query`, `category`, `articleNumber` and cursor pagination, and GET /api/v1/positions/{positionId}.
    • `unitPriceCents` is the stored net sales price and is omitted when the member who created the API key may not see sales prices. Purchase prices and calculations are not exposed.
    • `isPriceQuantityDependent` tells you that a calculation sets the price per quantity. Do not multiply `unitPriceCents` by a quantity in that case.
  109. Customer request and member webhooks
    v1
    • Added customer_request.created, customer_request.updated, customer_request.converted and customer_request.archived with full public request records. Internal-only changes do not emit updates.
    • Added member.created, member.updated and member.deactivated with the same payload as the members list. Account name changes notify each organization; reactivation is an update. Support access is excluded.
    • Customer requests now expose nullable `lostAt`. Marking a request lost and reopening it emit customer_request.updated; updatedAt alone does not emit an event.
    • Added guidance for subscribing instead of polling, recovering with updatedAfter, and reloading members after an outage.
  110. Rate limits per plan and RateLimit headers
    v1
    • Rate limits now depend on the plan. Business organizations get 10,000 requests per day (Berlin calendar day, previously 3,000 per UTC day) and 120 requests per minute and API key (previously 60). See Errors & conventions for the full table.
    • Every response to a recognized API key, including errors, now carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` for the daily quota; a 429 caused by the per-minute limit reports that limit instead. A `429 rate_limited` response still carries `Retry-After` in seconds.
    • A 429 from the separate file upload quota (`POST /api/v1/files`) now carries `details.quota: "file_uploads"` and no `RateLimit-*` headers.
    • The per-minute limit is now a fixed one-minute window instead of a token bucket with a burst of 20.
    • Organizations without API access no longer count requests against a daily quota; they keep receiving `403 plan_feature_required`.
  111. Pausing API keys
    v1
    • Organization owners can now pause an API key in the settings and reactivate it later without creating a new key. Every request with a paused key, including `GET /api/v1/me`, is rejected with `403 api_key_paused`. Paused requests do not count against any rate limit.
    • Owners see per-key usage for the last 30 days in the settings: requests per day, the five most used endpoints, the share of errors and rate-limited (`429`) requests counted separately.
  112. Downloadable OpenAPI and AI-readable documentation
    v1
    • Added public GET /api/v1/openapi.json without an API key or rate limit. The downloaded specification always names the production API host.
    • The API reference now includes server-rendered endpoints readable without JavaScript, alongside Scalar and a specification download link.
    • Added /developers/llms.txt with a plain-text overview and links to the portal and specification.
  113. Project origin and board visibility
    v1
    • Project responses now include `customerRequestId` (or null) and `visibleOnBoard`. `project.*` webhooks carry both fields when the change was made through this API.
    • Use `GET /api/v1/projects?visibleOnBoard=true` to match project board eligibility, including its archive view. Draft projects linked to an unconverted customer request are excluded. The unfiltered list is unchanged.
  114. Time entries and member roles
    v1
    • Added `GET /api/v1/time-entries` and `POST /api/v1/time-entries`: list and book working time for a member on a project (`projectId`) or an order (`orderId`). Entries are created in `draft` status and follow the app's rules: the API key's member must be allowed to book time for the target member, overlapping entries answer `409 conflict` unless `allowOverlap` is set.
    • Added `GET /api/v1/time-entry-categories` and the webhook event `time_entry.created`.
    • `GET /api/v1/members` now returns `role` (`owner`, `office`, `site_manager`, `technician`) and `memberType` (`field`, `office`) per member — the same vocabulary as `/api/v1/me`. Contact details and permissions remain excluded.
  115. File uploads
    v1
    • Added `POST /api/v1/files`: upload a file (raw request body, max. 20 MB, common image and document types) and receive a file id. Unused uploads expire after 24 hours.
    • File fields in `POST /api/v1/form-submissions` now accept an array of file ids as their answer value (max. 5 per field, each id usable once). Previously file fields were rejected with `file_upload_not_supported`.
    • Added `POST /api/v1/customer-requests/{customerRequestId}/files` to attach an uploaded file directly to a customer request.
  116. Customer requests
    v1
    • Added customer requests as a first-class REST resource: list, create, get, patch, and delete (archive) under `/api/v1/customer-requests`.
    • Creating a customer request now places a Kanban card on the organization's customer request board. `POST /api/v1/form-submissions` with the form option "Create customer request" enabled does the same — those requests are readable via this resource.
    • POST `/api/v1/customer-requests/{customerRequestId}/offers` creates a new draft quote for the request. Webhooks fire once the quote is finalized, not for drafts.
  117. Public API launch
    v1
    • Published the first stable v1 contract for customers, projects, tasks, members, documents, forms, and webhook subscriptions.
    • Standardized success and error envelopes, cursor pagination, public field whitelists, idempotency, and rate-limit responses.
    • Added signed webhook delivery with retries, secret rotation, wildcard subscriptions, and automatic endpoint disabling.
    • Published this Developer Portal and the complete Scalar OpenAPI reference.