{
  "openapi": "3.1.0",
  "info": {
    "title": "machs-dir-selbst Public API",
    "version": "1.0.0",
    "description": "\nConnect your own systems to **machs-dir-selbst**, the planning tool for PV systems: read and change customers,\nprojects, offers, your product catalog, services and bookings, and get a webhook when something changes.\n\n## Base URL\n\n```\nhttps://lmayqrxmxkxmaztaxvxu.supabase.co/functions/v1/api\n```\n\nEvery path in this reference starts there, e.g. `/v1/customers`.\n\n## Authentication\n\nSend your API key in the `Authorization` header: `Authorization: Bearer mds_live_...`. Create keys in the\nplanner under **Einstellungen → API-Schlüssel**. Each key has **scopes** that decide what it may read and change\n(`read:offers`, `write:offers`, …); a write scope includes reading. A key only ever sees its own company's data.\n\n## How the data fits together\n\n- A **customer** has **projects**: one per installation site, each with its roofs.\n- A **project** has **offers**. An offer is made of lines (modules on the roofs, inverter, battery, wallbox,\n  services, other material, free cost lines), and each line carries the net price it is sold for.\n- An offer starts as a **draft**, is **published** to the customer, and is then **accepted** or **declined**.\n- **Materials** (your product catalog), **manufacturers** and **services** are what offers are made of.\n- **Offer requests** (Anfragen) come in through your configurator and lead to offers.\n\n## Conventions\n\n- **Ids** are strings such as `\"812\"`; bookings and team members have uuids.\n- **Lists** take `limit` (1–200, default 50) and `offset` and answer with `{ data, total, limit, offset }`;\n  `total` counts all matches. `sort` takes a field and a direction, e.g. `sort=createdAt.desc`.\n- **Prices** are in euros. Offer lines and purchase prices are net. A material has a `purchasePrice` (what you\n  pay) and `sellingPrices` (what it sells for at each site, `net` and `gross`). `offerPrice` includes VAT.\n- **Times** are ISO 8601; dates are days on the German calendar.\n- **Errors** look like `{ \"error\": \"not_found\", \"message\": \"…\" }` with a matching HTTP status: `400` invalid\n  request, `401` missing or wrong API key, `403` scope missing, `404` not found in your company, `409` not\n  possible in the record's current state, `500`/`502` server error. Branch on `error`, not on `message`.\n",
    "contact": {
      "name": "machs-dir-selbst",
      "url": "https://machsdirselbst.solar"
    }
  },
  "servers": [
    {
      "url": "https://lmayqrxmxkxmaztaxvxu.supabase.co/functions/v1/api",
      "description": "Preview PR #416"
    }
  ],
  "tags": [
    {
      "name": "Customers",
      "description": "The people you plan PV systems for. A customer can have several projects."
    },
    {
      "name": "Projects",
      "description": "One installation site of a customer, with its roofs. You make offers per project."
    },
    {
      "name": "Offers",
      "description": "Offers for a project. Send an offer with all its lines in one call, or start with an empty draft and add lines one by one. Then publish it; the customer accepts or declines. An offer goes `DRAFT` → `PUBLISHED` → `ACCEPTED` or `DECLINED`."
    },
    {
      "name": "Offer services",
      "description": "The services on an offer, such as installation or scaffolding. Add, change and remove them one by one while the offer is a draft."
    },
    {
      "name": "Offer additional costs",
      "description": "Free cost lines on an offer, for a surcharge or, with a negative price, a discount. Changeable while the offer is a draft."
    },
    {
      "name": "Offer batteries",
      "description": "The batteries on an offer: the one you offer (`active`) and alternatives. Changeable while the offer is a draft."
    },
    {
      "name": "Offer wallboxes",
      "description": "The wallboxes (EV chargers) on an offer. Changeable while the offer is a draft."
    },
    {
      "name": "Offer misc materials",
      "description": "Miscellaneous material on an offer, such as cable or small parts. Changeable while the offer is a draft."
    },
    {
      "name": "Offer requests",
      "description": "Requests (Anfragen) customers send you, usually through your configurator, before you make them an offer."
    },
    {
      "name": "PV modules",
      "description": "The PV modules in your catalog. Their electrical data is needed to plan the strings, so it is required."
    },
    {
      "name": "Batteries",
      "description": "The battery storage products in your catalog."
    },
    {
      "name": "Inverters",
      "description": "The inverters in your catalog, with their MPP trackers (`mppTrackers`), which offers wire the module strings to."
    },
    {
      "name": "Wallboxes",
      "description": "The wallboxes (EV chargers) in your catalog."
    },
    {
      "name": "Equipment",
      "description": "Equipment that goes with modules, inverters, batteries, wallboxes or mounting systems on an offer."
    },
    {
      "name": "Misc materials",
      "description": "Miscellaneous material such as cable or small parts. It needs no manufacturer and can be put on new offers automatically."
    },
    {
      "name": "Subconstruction",
      "description": "Mounting systems (Unterkonstruktion), usually one per roof type."
    },
    {
      "name": "Emergency power",
      "description": "Emergency power products, offered together with an inverter."
    },
    {
      "name": "Material catalog",
      "description": "Across all material types: find a product by article number without knowing its type, and import a vendor's price list in one call."
    },
    {
      "name": "Manufacturers",
      "description": "The manufacturers of the products in your catalog."
    },
    {
      "name": "Services",
      "description": "The services you offer, such as installation or scaffolding, ready to add to offers."
    },
    {
      "name": "Analytics",
      "description": "Read-only reporting over your own data: offers and requests over time, request conversion rates, customer demographics, and what you sold or still have sitting in open offers. Everything is scoped to your company."
    },
    {
      "name": "Documents",
      "description": "Generate paperwork for an offer on demand: the grid operator's (Netzbetreiber) registration forms, and the string plan. Nothing is stored — each call renders from the offer as it stands."
    },
    {
      "name": "Grid operators",
      "description": "The grid operators (Netzbetreiber) you register installations with, the registration forms configured per operator, and the electrical contractors you can name on them. Read-only lookups that supply the ids the document-generation endpoints take."
    },
    {
      "name": "Company",
      "description": "Your company: its profile, your team members and your sites (Standorte). Projects, offers and bookings take the ids of members and sites."
    },
    {
      "name": "Bookings",
      "description": "Appointments your customers booked, and the kinds of appointment on offer. Read them, enter one on a customer's behalf, book a follow-up for a customer who already has one, and move, cancel or close one out — cancelling refunds and mails the customer the same way the planner does. Availability and schedules stay in the planner."
    },
    {
      "name": "Webhooks",
      "description": "Events we POST to your endpoints when data changes, so you can react without polling. Each event shares an envelope (`id`, `type`, `createdAt`, `data`); `data` is a compact set of the affected record's key fields — fetch the full object via the REST API. Deliveries are signed per Standard Webhooks; see the Webhooks guide for verification, retries and setup."
    }
  ],
  "x-tagGroups": [
    {
      "name": "Customers",
      "tags": [
        "Customers"
      ]
    },
    {
      "name": "Projects",
      "tags": [
        "Projects"
      ]
    },
    {
      "name": "Offer requests",
      "tags": [
        "Offer requests"
      ]
    },
    {
      "name": "Offers",
      "tags": [
        "Offers",
        "Offer services",
        "Offer additional costs",
        "Offer batteries",
        "Offer wallboxes",
        "Offer misc materials"
      ]
    },
    {
      "name": "Materials",
      "tags": [
        "Material catalog",
        "PV modules",
        "Batteries",
        "Inverters",
        "Wallboxes",
        "Equipment",
        "Misc materials",
        "Subconstruction",
        "Emergency power"
      ]
    },
    {
      "name": "Manufacturers",
      "tags": [
        "Manufacturers"
      ]
    },
    {
      "name": "Services",
      "tags": [
        "Services"
      ]
    },
    {
      "name": "Documents",
      "tags": [
        "Documents",
        "Grid operators"
      ]
    },
    {
      "name": "Analytics",
      "tags": [
        "Analytics"
      ]
    },
    {
      "name": "Company",
      "tags": [
        "Company"
      ]
    },
    {
      "name": "Bookings",
      "tags": [
        "Bookings"
      ]
    },
    {
      "name": "Webhooks",
      "tags": [
        "Webhooks"
      ]
    }
  ],
  "webhooks": {
    "project.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "project.created",
        "description": "A new project was created for one of your customers.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "project.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "project.updated",
        "description": "A project was changed. Read it with `GET /v1/projects/{id}` for the current values.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.accepted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.accepted",
        "description": "An offer was accepted, by the customer or by your team. A good moment to start the order or the invoice.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferAcceptedWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.state_changed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.state_changed",
        "description": "An offer changed its state, e.g. from `DRAFT` to `PUBLISHED` or from `PUBLISHED` to `ACCEPTED`. Carries the previous and the new state.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferStateChangedWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "project.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "project.deleted",
        "description": "A project was deleted. Its offers go with it, so this is the last event you will see for any of them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "customer.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "customer.created",
        "description": "A customer was added.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "customer.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "customer.updated",
        "description": "A customer's details changed. Only fields the API exposes count as a change — the offer and project counters do not raise this event.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "customer.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "customer.deleted",
        "description": "A customer was deleted.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.created",
        "description": "An offer was created. It starts as a DRAFT and is not visible to the customer yet.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.updated",
        "description": "An offer was edited. State transitions are reported by offer.state_changed instead, and a price recalculated from its line items does not raise this event.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.published": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.published",
        "description": "An offer became visible to the customer. Fires alongside offer.state_changed for the transition to PUBLISHED.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferPublishedWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "offer.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "offer.deleted",
        "description": "An offer was deleted. Not sent when the offer disappears because its project was deleted — project.deleted covers that.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "material.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "material.created",
        "description": "A product was added to your catalog. `materialType` says which catalog.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MaterialWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "material.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "material.updated",
        "description": "A product was renamed, archived or moved to another manufacturer. A new purchase price alone does not raise this event.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MaterialWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "material.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "material.deleted",
        "description": "A product was removed from your catalog. Products in use by an offer cannot be deleted, only archived.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MaterialWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.created",
        "description": "An appointment became a real booking. A free type and a manually entered one fire this the moment they are made; a paid one only once the payment clears. A checkout the customer abandons never fires it at all, so you never have to retract a booking. `source` says where it came from, and `offerRequestId` links a configurator booking to the offer request it produced. A follow-up fires it too; its `followUpOf` names the booking it follows, which is how you tell the next appointment of a customer you already have from a new one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.rescheduled": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.rescheduled",
        "description": "A confirmed appointment moved to another slot, by the customer or by your team. Carries `previousStartsAt` so you can update an existing calendar entry rather than creating a second one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingRescheduledWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.reassigned": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.reassigned",
        "description": "A confirmed appointment was handed to another planner. `planner` is who has it now, `previousPlanner` who had it before — useful for moving a task or a calendar entry between people. The customer keeps their time and is not notified.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingReassignedWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.cancelled": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.cancelled",
        "description": "An appointment was cancelled and its slot released. `refunded` says whether money went back; the amount arrives with booking.payment_refunded.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingCancelledWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.completed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.completed",
        "description": "An appointment was marked as having taken place.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.no_show": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.no_show",
        "description": "The customer did not turn up for their appointment.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.payment_paid": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.payment_paid",
        "description": "A paid appointment was settled — through Stripe, or marked paid by your team for a booking settled outside it (`paymentMethod: manual`).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingPaymentPaidWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    },
    "booking.payment_refunded": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "booking.payment_refunded",
        "description": "A paid appointment was refunded, usually alongside a cancellation.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingPaymentRefundedWebhookEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Answer with any 2xx status to confirm receipt. Any other status, or no answer within 10 seconds, counts as a failed delivery and is retried."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API Key",
        "description": "Send your API key as a Bearer token: `Authorization: Bearer mds_live_...`. Create keys in the planner under Einstellungen → API-Schlüssel."
      }
    },
    "schemas": {
      "ProjectWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "project.created"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The project's id. Read the whole project with `GET /v1/projects/{id}`.",
                "example": "41"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Project name.",
                "example": "PV Müller Satteldach"
              },
              "customerId": {
                "type": "string",
                "description": "The customer the project belongs to.",
                "example": "77"
              },
              "createdAt": {
                "type": "string",
                "description": "When the project was created.",
                "example": "2026-07-01T10:25:00Z"
              }
            },
            "required": [
              "id",
              "customerId",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "OfferAcceptedWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "offer.accepted"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The offer's id. Read it with `GET /v1/offers/{id}`.",
                "example": "812"
              },
              "projectId": {
                "type": "string",
                "description": "The offer's project.",
                "example": "41"
              },
              "state": {
                "type": "string",
                "description": "Always `ACCEPTED` for this event.",
                "example": "ACCEPTED"
              },
              "acceptedAt": {
                "type": "string",
                "description": "When the offer was accepted.",
                "example": "2026-05-20T12:34:56.789Z"
              },
              "createdAt": {
                "type": "string",
                "description": "When the offer was created.",
                "example": "2026-05-01T09:00:00Z"
              }
            },
            "required": [
              "id",
              "projectId",
              "state",
              "acceptedAt",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "OfferStateChangedWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "offer.state_changed"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The offer's id.",
                "example": "812"
              },
              "projectId": {
                "type": "string",
                "description": "The offer's project.",
                "example": "41"
              },
              "previousState": {
                "type": "string",
                "description": "The state before the change.",
                "example": "PUBLISHED"
              },
              "state": {
                "type": "string",
                "description": "The new state.",
                "example": "ACCEPTED"
              },
              "createdAt": {
                "type": "string",
                "description": "When the offer was created.",
                "example": "2026-05-01T09:00:00Z"
              }
            },
            "required": [
              "id",
              "projectId",
              "previousState",
              "state",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "OfferWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "offer.created"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The offer's id. Read it with `GET /v1/offers/{id}`.",
                "example": "812"
              },
              "projectId": {
                "type": "string",
                "description": "The offer's project.",
                "example": "41"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Offer name.",
                "example": "Angebot PV 9,8 kWp"
              },
              "state": {
                "type": "string",
                "description": "The offer's state when the event happened.",
                "example": "DRAFT"
              },
              "isIndicationPrice": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "`true` for an indication offer, `false` for a detailed one.",
                "example": false
              },
              "createdAt": {
                "type": "string",
                "description": "When the offer was created.",
                "example": "2026-05-01T09:00:00Z"
              }
            },
            "required": [
              "id",
              "projectId",
              "state",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "OfferPublishedWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "offer.published"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The offer's id.",
                "example": "812"
              },
              "projectId": {
                "type": "string",
                "description": "The offer's project.",
                "example": "41"
              },
              "state": {
                "type": "string",
                "description": "Always `PUBLISHED` for this event.",
                "example": "PUBLISHED"
              },
              "validTo": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The last day the customer can accept the offer.",
                "example": "2026-08-15T00:00:00Z"
              },
              "createdAt": {
                "type": "string",
                "description": "When the offer was created.",
                "example": "2026-05-01T09:00:00Z"
              }
            },
            "required": [
              "id",
              "projectId",
              "state",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "CustomerWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "customer.created"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The customer's id. Read the whole customer with `GET /v1/customers/{id}`.",
                "example": "77"
              },
              "firstName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First name.",
                "example": "Anna"
              },
              "lastName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Last name.",
                "example": "Müller"
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Email address.",
                "example": "anna.mueller@example.com"
              },
              "createdAt": {
                "type": "string",
                "description": "When the customer was created.",
                "example": "2026-07-01T10:22:00Z"
              }
            },
            "required": [
              "id",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "MaterialWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "material.created"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The material's id. Read it with `GET /v1/materials/{type}/{id}`.",
                "example": "150"
              },
              "materialType": {
                "type": "string",
                "description": "The material type: `pv_module`, `battery`, `inverter`, `wallbox`, `equipment`, `misc`, `subconstruction` or `emergency_power`.",
                "example": "pv_module"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Product name.",
                "example": "Aiko Neostar 2S 445W"
              },
              "archived": {
                "type": [
                  "boolean",
                  "null"
                ],
                "description": "Archived products stay on existing offers but cannot be added to new ones.",
                "example": false
              },
              "manufacturerId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The manufacturer.",
                "example": "12"
              }
            },
            "required": [
              "id",
              "materialType"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.created"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingReassignedWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.reassigned"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              },
              "previousPlanner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "Who had the appointment before. `null` when nobody had it."
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingRescheduledWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.rescheduled"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              },
              "previousStartsAt": {
                "type": "string",
                "description": "Where the appointment was before it moved.",
                "example": "2026-09-15T10:00:00Z"
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt",
              "previousStartsAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingCancelledWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.cancelled"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              },
              "cancellationReason": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The reason given.",
                "example": "Termin passt nicht mehr."
              },
              "cancelledAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the booking was cancelled."
              },
              "refunded": {
                "type": "boolean",
                "description": "Whether money went back to the customer. The amount comes with `booking.payment_refunded`.",
                "example": true
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt",
              "refunded"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingPaymentPaidWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.payment_paid"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              },
              "paidAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the payment went through."
              },
              "paymentMethod": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "`card`, `paypal`, `sepa_debit`, or `manual`.",
                "example": "card"
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "BookingPaymentRefundedWebhookEvent": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The event's id. The same event can arrive twice; skip ids you have handled already.",
            "example": "a1b2c3d4-e5f6-4890-a1b2-c3d4e5f60789"
          },
          "type": {
            "type": "string",
            "description": "The event name.",
            "example": "booking.payment_refunded"
          },
          "createdAt": {
            "type": "string",
            "description": "When the event was sent (ISO 8601).",
            "example": "2026-05-20T12:34:56.789Z"
          },
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "The booking's id. Read the whole booking with `GET /v1/bookings/{id}`.",
                "example": "a0000000-0000-4000-8000-000000000001"
              },
              "source": {
                "type": "string",
                "description": "`BUILDER`, `BOOKING_PAGE` or `PLANNER`.",
                "example": "BUILDER"
              },
              "typeSlug": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The appointment type.",
                "example": "erstgespraech"
              },
              "typeName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Name of the appointment type.",
                "example": "Erstgespräch"
              },
              "startsAt": {
                "type": "string",
                "description": "Start.",
                "example": "2026-09-15T10:00:00Z"
              },
              "endsAt": {
                "type": "string",
                "description": "End.",
                "example": "2026-09-15T10:30:00Z"
              },
              "status": {
                "type": "string",
                "description": "The booking's status.",
                "example": "CONFIRMED"
              },
              "paymentStatus": {
                "type": "string",
                "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
                "example": "PAID"
              },
              "priceCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "Price in cents, including VAT.",
                "example": 7900
              },
              "amountPaidCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "What was actually paid, in cents. Less than `priceCents` when a promotion code was used; invoice this amount. `null` for free and manually settled bookings.",
                "example": 7110
              },
              "currency": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Currency.",
                "example": "EUR"
              },
              "taxRate": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "The VAT percentage contained in `priceCents`. `0` means no VAT.",
                "example": 19
              },
              "customer": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name, always filled.",
                    "example": "Anna Müller"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "anna.mueller@example.com"
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number.",
                    "example": "+49 89 1234567"
                  },
                  "forename": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "First name as typed; `null` when your team entered the booking.",
                    "example": "Anna"
                  },
                  "surname": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Last name as typed; `null` when your team entered the booking.",
                    "example": "Müller"
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address as the customer typed it, in one line.",
                    "example": "Lindenstraße 12, 80331 München"
                  },
                  "billingName": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Who the invoice goes to, as confirmed in the Stripe checkout."
                  },
                  "billingAddress": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "line1": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Street and house number.",
                        "example": "Musterstraße 12"
                      },
                      "line2": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Address addition."
                      },
                      "postalCode": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Postal code.",
                        "example": "04109"
                      },
                      "city": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Town or city.",
                        "example": "Leipzig"
                      },
                      "state": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Region; usually `null` for German addresses."
                      },
                      "country": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Country code (ISO 3166-1 alpha-2).",
                        "example": "DE"
                      }
                    },
                    "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
                  }
                }
              },
              "invoice": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "number": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice number.",
                    "example": "ABCD-0001"
                  },
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Invoice page at Stripe."
                  },
                  "pdfUrl": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Direct link to the PDF."
                  }
                },
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting from this event, so this is `null`."
              },
              "planner": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "The team member's id.",
                    "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Name.",
                    "example": "Max Mustermann"
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address.",
                    "example": "max@solario.example"
                  }
                },
                "description": "The team member the appointment is assigned to. `null` while nobody is; it changes when the booking is handed to someone else."
              },
              "stripe": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "customerId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe customer.",
                    "example": "cus_QwErTy123456"
                  },
                  "paymentIntentId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe payment.",
                    "example": "pi_3PabcdEFGH123456"
                  },
                  "invoiceId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                    "example": "in_1PabcdEFGH123456"
                  }
                },
                "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
              },
              "offerRequestId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The offer request of a `BUILDER` booking, once it exists. A follow-up carries the offer request of the booking it follows.",
                "example": "501"
              },
              "followUpOf": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The booking this one follows up; `null` for every other booking. This tells the next appointment of a customer you already have from a new customer.",
                "example": "a0000000-0000-4000-8000-000000000003"
              },
              "createdAt": {
                "type": "string",
                "description": "When the booking was made.",
                "example": "2026-09-01T08:15:00Z"
              },
              "refundedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the money went back."
              },
              "refundAmountCents": {
                "type": [
                  "integer",
                  "null"
                ],
                "description": "How much went back, in cents.",
                "example": 7900
              }
            },
            "required": [
              "id",
              "source",
              "startsAt",
              "endsAt",
              "status",
              "paymentStatus",
              "createdAt"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "Address": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "zip": {
            "type": [
              "string",
              "null"
            ],
            "description": "Postal code.",
            "example": "80331"
          },
          "city": {
            "type": [
              "string",
              "null"
            ],
            "description": "Town or city.",
            "example": "München"
          },
          "street": {
            "type": [
              "string",
              "null"
            ],
            "description": "Street name.",
            "example": "Lindenstraße"
          },
          "streetNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "House number.",
            "example": "12"
          }
        },
        "description": "Address of the installation site."
      },
      "ProjectRoof": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The roof's id. Offers refer to it as `projectRoofConfigurationId`.",
            "example": "301"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Roof name.",
            "example": "Süddach"
          },
          "azimuth": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Compass direction the roof faces, in degrees: 0 north, 90 east, 180 south, 270 west.",
            "example": 180
          },
          "tilt": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Roof pitch in degrees, from 0 (flat) to 90.",
            "example": 35
          },
          "pvCloudingType": {
            "type": [
              "string",
              "null"
            ],
            "description": "How much the roof is shaded: `NO_CLOUDING`, `LITTLE_CLOUDING`, `MUCH_CLOUDING` or `VERY_MUCH_CLOUDING`.",
            "example": "NO_CLOUDING"
          },
          "roofType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "TILED_ROOF",
              "BITUMEN_ROOF",
              "TRAPEZOIDAL_SHEET_METAL_ROOF",
              "BEADED_PLATE_ROOF",
              "FACADE",
              "OPEN_FIELD"
            ],
            "description": "What the roof is covered with: `TILED_ROOF`, `BITUMEN_ROOF`, `TRAPEZOIDAL_SHEET_METAL_ROOF`, `BEADED_PLATE_ROOF`, `FACADE` or `OPEN_FIELD`. The mounting system has to suit it.",
            "example": "TILED_ROOF"
          },
          "areaM2": {
            "type": [
              "number",
              "null"
            ],
            "description": "Area of the roof outline drawn in the planner, in m²; `null` when none was drawn.",
            "example": 48.6
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of a picture of the roof, if one was uploaded.",
            "example": "1/roofs/suedach.jpg"
          }
        },
        "required": [
          "id"
        ],
        "description": "A roof of the project."
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The project's id.",
            "example": "41"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Project name.",
            "example": "PV Müller Satteldach"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text notes.",
            "example": "Zählerschrank 2019 erneuert."
          },
          "customerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The customer the project belongs to.",
            "example": "77"
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the project stands: `INDICATION_PHASE` (first rough estimate, Vorplanung), `DETAIL_PHASE` (detailed planning and offer), `IN_IMPLEMENTATION` (sold, being installed) or `FINISHED`.",
            "example": "DETAIL_PHASE"
          },
          "address": {
            "$ref": "#/components/schemas/Address"
          },
          "customerFullName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The customer's full name.",
            "example": "Anna Müller"
          },
          "companyMemberName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The team member responsible for the project.",
            "example": "Max Mustermann"
          },
          "companySiteName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The company site (Standort) the project belongs to; its margins and VAT rate apply to the project's offers.",
            "example": "München"
          },
          "offersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "All offers of the project, drafts included.",
            "example": 2
          },
          "activeOffersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Offers the customer has been shown: published, accepted or declined ones. Drafts do not count.",
            "example": 1
          },
          "acceptedOfferId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The accepted detailed offer, if there is one.",
            "example": "812"
          },
          "networkCarrierId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The grid operator (Netzbetreiber) responsible for the site. `GET /v1/grid-operators/{id}` gives its name and forms; it is also the operator the registration forms are made for by default.",
            "example": "7"
          },
          "roofs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectRoof"
            },
            "description": "The project's roofs, oldest first."
          },
          "createdAt": {
            "type": "string",
            "description": "When the project was created (ISO 8601).",
            "example": "2026-07-01T10:25:00+00:00"
          }
        },
        "required": [
          "id",
          "roofs",
          "createdAt"
        ],
        "description": "A PV project: one installation site of a customer, with its roofs and the offers you make for it."
      },
      "ProjectList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Project"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error code. Branch on this; it does not change.",
            "example": "not_found"
          },
          "message": {
            "type": "string",
            "description": "What went wrong, for a person to read. Not every error has one, and the wording may change.",
            "example": "Only draft offers can be replaced. Create a new offer instead."
          },
          "required": {
            "type": "string",
            "description": "With `insufficient_scope`: the scope the endpoint needs.",
            "example": "write:offers"
          }
        },
        "required": [
          "error"
        ],
        "description": "Every error response has this shape."
      },
      "RoofInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The id of an existing roof to change (`roofs[].id` of the project). Leave it out to add a new roof.",
            "example": "301"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "Roof name.",
            "example": "Süddach"
          },
          "azimuth": {
            "type": "integer",
            "minimum": 0,
            "maximum": 360,
            "description": "Compass direction the roof faces, in degrees: 0 north, 90 east, 180 south, 270 west.",
            "example": 180
          },
          "tilt": {
            "type": "integer",
            "minimum": 0,
            "maximum": 90,
            "description": "Roof pitch in degrees, from 0 (flat) to 90.",
            "example": 35
          },
          "pvCloudingType": {
            "type": "string",
            "enum": [
              "NO_CLOUDING",
              "LITTLE_CLOUDING",
              "MUCH_CLOUDING",
              "VERY_MUCH_CLOUDING"
            ],
            "description": "How much the roof is shaded, from `NO_CLOUDING` to `VERY_MUCH_CLOUDING`.",
            "example": "LITTLE_CLOUDING"
          },
          "roofType": {
            "type": "string",
            "enum": [
              "TILED_ROOF",
              "BITUMEN_ROOF",
              "TRAPEZOIDAL_SHEET_METAL_ROOF",
              "BEADED_PLATE_ROOF",
              "FACADE",
              "OPEN_FIELD"
            ],
            "description": "What the roof is covered with: `TILED_ROOF`, `BITUMEN_ROOF`, `TRAPEZOIDAL_SHEET_METAL_ROOF`, `BEADED_PLATE_ROOF`, `FACADE` or `OPEN_FIELD`. The mounting system on an offer has to suit it. Leave it out to keep an existing roof's type; a new roof without one is a tiled roof (`TILED_ROOF`).",
            "example": "TILED_ROOF"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of a picture of the roof, if one was uploaded.",
            "example": "1/roofs/suedach.jpg"
          }
        },
        "required": [
          "name",
          "azimuth",
          "tilt",
          "pvCloudingType"
        ],
        "description": "A roof of the project. Offers place modules on these roofs."
      },
      "ProjectInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "Project name.",
            "example": "PV Müller Satteldach"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text notes.",
            "example": "Zählerschrank 2019 erneuert."
          },
          "customerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The customer the project belongs to (`GET /v1/customers`).",
            "example": "77"
          },
          "associatedCompanyMemberId": {
            "type": "string",
            "format": "uuid",
            "description": "The team member responsible for the project (`GET /v1/company/members`).",
            "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
          },
          "associatedCompanySiteId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The company site (Standort) the project belongs to (`GET /v1/company/sites`). Its margins and VAT rate apply to the project's offers.",
            "example": "1"
          },
          "networkCarrierId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The grid operator (Netzbetreiber) of the site, from `GET /v1/grid-operators`.",
            "example": "7"
          },
          "electricityPriceCentPerKwh": {
            "type": "number",
            "minimum": 10,
            "description": "What the customer pays for electricity, in cents per kWh (at least 10). Used for the savings calculation.",
            "example": 35
          },
          "customerElectricityConsumptionKwhPerYear": {
            "type": "integer",
            "minimum": 0,
            "description": "The customer's yearly electricity consumption in kWh.",
            "example": 4500
          },
          "address": {
            "type": "object",
            "properties": {
              "zip": {
                "type": "string",
                "description": "Postal code.",
                "example": "80331"
              },
              "city": {
                "type": "string",
                "description": "Town or city.",
                "example": "München"
              },
              "street": {
                "type": "string",
                "description": "Street name.",
                "example": "Lindenstraße"
              },
              "streetNumber": {
                "type": "string",
                "description": "House number.",
                "example": "12"
              }
            },
            "description": "Address of the installation site. Send only the parts you want to set."
          },
          "roofs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoofInput"
            },
            "description": "The project's roofs. This is always the complete list: when changing a project, roofs you leave out are deleted. Leave `roofs` out to keep the roofs as they are."
          }
        },
        "required": [
          "name",
          "customerId",
          "associatedCompanyMemberId",
          "electricityPriceCentPerKwh",
          "customerElectricityConsumptionKwhPerYear"
        ],
        "description": "A project to create (`POST`) or change (`PATCH`), saved together with its roofs. Creating needs `name`, `customerId`, `associatedCompanyMemberId`, `electricityPriceCentPerKwh` and `customerElectricityConsumptionKwhPerYear`; when changing, every field is optional.",
        "example": {
          "name": "PV Müller Satteldach",
          "customerId": "77",
          "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
          "associatedCompanySiteId": "1",
          "electricityPriceCentPerKwh": 35,
          "customerElectricityConsumptionKwhPerYear": 4500,
          "address": {
            "street": "Lindenstraße",
            "streetNumber": "12",
            "zip": "80331",
            "city": "München"
          },
          "roofs": [
            {
              "name": "Süddach",
              "azimuth": 180,
              "tilt": 35,
              "pvCloudingType": "NO_CLOUDING",
              "roofType": "TILED_ROOF"
            }
          ]
        }
      },
      "ProjectUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "Project name.",
            "example": "PV Müller Satteldach"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text notes.",
            "example": "Zählerschrank 2019 erneuert."
          },
          "customerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The customer the project belongs to (`GET /v1/customers`).",
            "example": "77"
          },
          "associatedCompanyMemberId": {
            "type": "string",
            "format": "uuid",
            "description": "The team member responsible for the project (`GET /v1/company/members`).",
            "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
          },
          "associatedCompanySiteId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The company site (Standort) the project belongs to (`GET /v1/company/sites`). Its margins and VAT rate apply to the project's offers.",
            "example": "1"
          },
          "networkCarrierId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The grid operator (Netzbetreiber) of the site, from `GET /v1/grid-operators`.",
            "example": "7"
          },
          "electricityPriceCentPerKwh": {
            "type": "number",
            "minimum": 10,
            "description": "What the customer pays for electricity, in cents per kWh (at least 10). Used for the savings calculation.",
            "example": 35
          },
          "customerElectricityConsumptionKwhPerYear": {
            "type": "integer",
            "minimum": 0,
            "description": "The customer's yearly electricity consumption in kWh.",
            "example": 4500
          },
          "address": {
            "type": "object",
            "properties": {
              "zip": {
                "type": "string",
                "description": "Postal code.",
                "example": "80331"
              },
              "city": {
                "type": "string",
                "description": "Town or city.",
                "example": "München"
              },
              "street": {
                "type": "string",
                "description": "Street name.",
                "example": "Lindenstraße"
              },
              "streetNumber": {
                "type": "string",
                "description": "House number.",
                "example": "12"
              }
            },
            "description": "Address of the installation site. Send only the parts you want to set."
          },
          "roofs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoofInput"
            },
            "description": "The project's roofs. This is always the complete list: when changing a project, roofs you leave out are deleted. Leave `roofs` out to keep the roofs as they are."
          }
        },
        "description": "Changes to a project. Send only the fields to change. If you send `roofs`, send all of them: roofs you leave out are deleted.",
        "example": {
          "customerElectricityConsumptionKwhPerYear": 6000,
          "electricityPriceCentPerKwh": 38
        }
      },
      "Offer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The offer's id.",
            "example": "812"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Offer name.",
            "example": "Angebot PV 9,8 kWp"
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project the offer belongs to.",
            "example": "41"
          },
          "state": {
            "type": "string",
            "description": "`DRAFT` (being prepared, not visible to the customer), `PUBLISHED` (the customer can see and accept it), `ACCEPTED` or `DECLINED`. Change it with `POST /v1/offers/{id}/state`.",
            "example": "PUBLISHED"
          },
          "isIndicationPrice": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for an indication offer (Vorplanung), a first rough price; `false` for a detailed offer.",
            "example": false
          },
          "offerPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Total price in euros including VAT, calculated from the offer's lines. Optional extras are not included.",
            "example": 18650
          },
          "validTo": {
            "type": [
              "string",
              "null"
            ],
            "description": "The last day the offer can be accepted: until the end of that day, German time (ISO 8601).",
            "example": "2026-10-31T00:00:00+00:00"
          },
          "customerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The customer of the offer's project.",
            "example": "77"
          },
          "projectName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the offer's project.",
            "example": "PV Müller Satteldach"
          },
          "customerFullName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Full name of the customer.",
            "example": "Anna Müller"
          },
          "createdAt": {
            "type": "string",
            "description": "When the offer was created (ISO 8601).",
            "example": "2026-07-03T09:10:00+00:00"
          }
        },
        "required": [
          "id",
          "state",
          "createdAt"
        ],
        "description": "An offer for a project. You create it as a draft, publish it, and the customer accepts or declines it."
      },
      "OfferList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offer"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "OfferInput": {
        "type": "object",
        "properties": {
          "offer": {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string",
                "pattern": "^\\d+$",
                "description": "The project the offer is for.",
                "example": "41"
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Offer name.",
                "example": "Angebot PV 9,8 kWp"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Introduction text shown on the offer.",
                "example": "Ihre PV-Anlage mit Speicher."
              },
              "footnote": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Small print shown at the end of the offer.",
                "example": "Preise gültig bei Auftrag bis 31.10."
              },
              "link3dView": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Link to a 3D view of the planned system, shown to the customer.",
                "example": "https://3d.example.com/view/abc123"
              },
              "validTo": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The last day the customer can accept the offer (until the end of that day, German time). Leave it out for today plus the offer validity set in the planner (14 days if none is set).",
                "example": "2026-10-31"
              },
              "wallboxInstallationCosts": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Net price in euros for installing the wallbox.",
                "example": 600
              },
              "fullFeedIn": {
                "type": "boolean",
                "description": "`true` when all the electricity is fed into the grid (Volleinspeisung) instead of being used in the house first. Changes the savings calculation.",
                "example": false
              },
              "applySalesTaxFreeEntitled": {
                "type": "boolean",
                "description": "Apply the 0 % VAT rate for PV systems to the lines that qualify for it.",
                "example": true
              },
              "isIndicationPrice": {
                "type": "boolean",
                "description": "`true` for an indication offer (Vorplanung), a first rough price; `false`, the default, for a detailed offer.",
                "example": false
              },
              "associatedCompanyMemberId": {
                "type": "string",
                "format": "uuid",
                "description": "The team member responsible for the offer (`GET /v1/company/members`).",
                "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
              },
              "taxPercent": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "VAT rate in percent. Leave it out to use the rate of the project's company site.",
                "example": 19
              }
            },
            "required": [
              "projectId",
              "associatedCompanyMemberId"
            ],
            "description": "The offer itself: project, name, validity and settings."
          },
          "previewImages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "imagePath": {
                  "type": "string",
                  "description": "Storage path of an uploaded image.",
                  "example": "1/offers/visualisierung.png"
                },
                "orderPriority": {
                  "type": "integer",
                  "description": "Position; lower comes first.",
                  "example": 0
                }
              },
              "required": [
                "imagePath"
              ]
            },
            "description": "Pictures shown at the top of the offer. Upload them first; the API takes the storage path."
          },
          "roofs": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "projectRoofConfigurationId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The project roof this entry is for (`roofs[].id` of `GET /v1/projects/{id}`).",
                  "example": "301"
                },
                "active": {
                  "type": "boolean",
                  "description": "Whether the roof is part of the offer. Default `false`: an inactive roof is listed but carries nothing.",
                  "example": true
                },
                "pvModulesCount": {
                  "type": "integer",
                  "description": "How many modules go on this roof. Default 0.",
                  "example": 22
                },
                "moduleConfigs": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "recommended": {
                        "type": "boolean",
                        "description": "Marks the module variant you offer for this roof; mark exactly one per roof. The others are alternatives the customer can choose instead.",
                        "example": true
                      },
                      "materialPvModuleId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "The PV module (`GET /v1/materials/pv-modules`).",
                        "example": "150"
                      },
                      "netPricePerModule": {
                        "type": "number",
                        "description": "Net price per module in euros. Without it the modules cost nothing on the offer.",
                        "example": 98
                      },
                      "equipment": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "equipmentId": {
                              "type": "string",
                              "pattern": "^\\d+$",
                              "description": "The equipment material (`GET /v1/materials/equipment`).",
                              "example": "33"
                            },
                            "optional": {
                              "type": "boolean",
                              "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                              "example": false
                            },
                            "netPrice": {
                              "type": "number",
                              "description": "Net price per piece in euros.",
                              "example": 24.9
                            },
                            "units": {
                              "type": "integer",
                              "description": "How many pieces. Default 1.",
                              "example": 1
                            }
                          },
                          "required": [
                            "equipmentId",
                            "netPrice"
                          ]
                        },
                        "description": "Equipment that comes with this module variant."
                      }
                    },
                    "required": [
                      "materialPvModuleId"
                    ]
                  },
                  "description": "The module variants for this roof. Mark the one you offer with `recommended`."
                },
                "subconstruction": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "properties": {
                    "materialSubconstructionId": {
                      "type": "string",
                      "pattern": "^\\d+$",
                      "description": "The mounting system (`GET /v1/materials/subconstruction`).",
                      "example": "9"
                    },
                    "netPrice": {
                      "type": "number",
                      "description": "Fixed net price in euros for the roof's mounting system.",
                      "example": 150
                    },
                    "pricePerModule": {
                      "type": "number",
                      "description": "Net price in euros per module on the roof, added to `netPrice`.",
                      "example": 32
                    },
                    "equipment": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "equipmentId": {
                            "type": "string",
                            "pattern": "^\\d+$",
                            "description": "The equipment material.",
                            "example": "34"
                          },
                          "optional": {
                            "type": "boolean",
                            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                            "example": false
                          },
                          "units": {
                            "type": "integer",
                            "description": "How many pieces. Default 1.",
                            "example": 1
                          },
                          "netPrice": {
                            "type": "number",
                            "description": "Net price per piece in euros.",
                            "example": 12
                          },
                          "netPricePerModule": {
                            "type": "number",
                            "description": "Net price in euros per module on the roof.",
                            "example": 0
                          }
                        },
                        "required": [
                          "equipmentId",
                          "netPrice"
                        ]
                      },
                      "description": "Equipment for the mounting system."
                    }
                  },
                  "required": [
                    "materialSubconstructionId",
                    "netPrice"
                  ],
                  "description": "The mounting system (Unterkonstruktion) for this roof, or `null` for none."
                }
              },
              "required": [
                "projectRoofConfigurationId"
              ]
            },
            "description": "For each roof of the project: whether it is used, the modules on it and the mounting system."
          },
          "batteries": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "active": {
                  "type": "boolean",
                  "description": "Marks the battery you offer. List more batteries without `active` as alternatives the customer can choose instead. Default `false`.",
                  "example": true
                },
                "materialBatteryId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The battery (`GET /v1/materials/batteries`).",
                  "example": "88"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price in euros.",
                  "example": 4200
                }
              },
              "required": [
                "materialBatteryId",
                "netPrice"
              ]
            },
            "description": "Battery storage. Mark the one you offer with `active`."
          },
          "batteryEquipment": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "optional": {
                  "type": "boolean",
                  "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                  "example": false
                },
                "units": {
                  "type": "integer",
                  "description": "How many pieces. Default 1.",
                  "example": 1
                },
                "equipmentId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The equipment material.",
                  "example": "35"
                },
                "materialBatteryId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The battery on this offer the equipment belongs to.",
                  "example": "88"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price per piece in euros.",
                  "example": 89
                }
              },
              "required": [
                "equipmentId",
                "materialBatteryId",
                "netPrice"
              ]
            },
            "description": "Equipment for the batteries."
          },
          "wallboxes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "active": {
                  "type": "boolean",
                  "description": "Marks the wallbox you offer. Default `false`.",
                  "example": true
                },
                "materialWallboxId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The wallbox (`GET /v1/materials/wallboxes`).",
                  "example": "25"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price in euros.",
                  "example": 790
                }
              },
              "required": [
                "materialWallboxId",
                "netPrice"
              ]
            },
            "description": "Wallboxes (EV chargers). Mark the one you offer with `active`."
          },
          "wallboxEquipment": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "optional": {
                  "type": "boolean",
                  "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                  "example": false
                },
                "units": {
                  "type": "integer",
                  "description": "How many pieces. Default 1.",
                  "example": 1
                },
                "equipmentId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The equipment material.",
                  "example": "36"
                },
                "materialWallboxId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The wallbox on this offer the equipment belongs to.",
                  "example": "25"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price per piece in euros.",
                  "example": 45
                }
              },
              "required": [
                "equipmentId",
                "materialWallboxId",
                "netPrice"
              ]
            },
            "description": "Equipment for the wallboxes."
          },
          "services": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "serviceId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The service (`GET /v1/services`).",
                  "example": "5"
                },
                "fixedPrice": {
                  "type": "number",
                  "description": "Net price in euros.",
                  "example": 450
                },
                "pricePerModule": {
                  "type": "number",
                  "description": "Net price in euros per module of the offer, added to `fixedPrice`. Default 0.",
                  "example": 0
                },
                "optional": {
                  "type": "boolean",
                  "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                  "example": false
                },
                "category": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "enum": [
                    "OTHER",
                    "ELECTRICIAN",
                    "ROOF"
                  ],
                  "description": "The section of the offer the service is listed in: `ROOF` (roof work), `ELECTRICIAN` (electrical work) or `OTHER`, the default.",
                  "example": "ROOF"
                },
                "orderPriority": {
                  "type": "integer",
                  "description": "Position in the list; lower comes first. Default 0.",
                  "example": 1
                }
              },
              "required": [
                "serviceId",
                "fixedPrice"
              ]
            },
            "description": "Services such as installation or scaffolding."
          },
          "misc": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "materialMiscId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The miscellaneous material (`GET /v1/materials/misc`).",
                  "example": "8"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price per piece in euros.",
                  "example": 3.5
                },
                "piecesCount": {
                  "type": "integer",
                  "description": "How many pieces.",
                  "example": 40
                },
                "pricePerModule": {
                  "type": "number",
                  "description": "Net price in euros per module of the offer, on top. Default 0.",
                  "example": 0
                },
                "optional": {
                  "type": "boolean",
                  "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                  "example": false
                },
                "equipment": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "equipmentId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "The equipment material (`GET /v1/materials/equipment`).",
                        "example": "33"
                      },
                      "optional": {
                        "type": "boolean",
                        "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                        "example": false
                      },
                      "netPrice": {
                        "type": "number",
                        "description": "Net price per piece in euros.",
                        "example": 24.9
                      },
                      "units": {
                        "type": "integer",
                        "description": "How many pieces. Default 1.",
                        "example": 1
                      }
                    },
                    "required": [
                      "equipmentId",
                      "netPrice"
                    ]
                  },
                  "description": "Equipment that comes with this material."
                }
              },
              "required": [
                "materialMiscId",
                "netPrice",
                "piecesCount"
              ]
            },
            "description": "Miscellaneous material, such as cable or small parts."
          },
          "inverters": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "materialInverterId": {
                  "type": "string",
                  "pattern": "^\\d+$",
                  "description": "The inverter (`GET /v1/materials/inverters`).",
                  "example": "70"
                },
                "netPrice": {
                  "type": "number",
                  "description": "Net price in euros.",
                  "example": 1850
                },
                "mpptCircuitTypes": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "materialInverterMppTrackerId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "One of the inverter's MPP trackers (`mppTrackers[].id` of `GET /v1/materials/inverters/{id}`).",
                        "example": "141"
                      },
                      "circuitType": {
                        "type": "string",
                        "enum": [
                          "SERIES",
                          "PARALLEL"
                        ],
                        "description": "How the strings on this tracker are combined.",
                        "example": "PARALLEL"
                      }
                    },
                    "required": [
                      "materialInverterMppTrackerId",
                      "circuitType"
                    ]
                  },
                  "description": "How the strings on each MPP tracker are combined. Every tracker of the inverter is on the offer already; an entry here only sets its circuit type, it does not add or remove trackers."
                },
                "equipment": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "equipmentId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "The equipment material (`GET /v1/materials/equipment`).",
                        "example": "33"
                      },
                      "optional": {
                        "type": "boolean",
                        "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                        "example": false
                      },
                      "netPrice": {
                        "type": "number",
                        "description": "Net price per piece in euros.",
                        "example": 24.9
                      },
                      "units": {
                        "type": "integer",
                        "description": "How many pieces. Default 1.",
                        "example": 1
                      }
                    },
                    "required": [
                      "equipmentId",
                      "netPrice"
                    ]
                  },
                  "description": "Equipment for the inverter."
                },
                "emergencyPowers": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Name of the emergency power solution on the offer.",
                        "example": "Ersatzstrom-Umschaltbox"
                      },
                      "description": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "Text shown on the offer.",
                        "example": "Automatische Umschaltung bei Netzausfall."
                      },
                      "emergencyPowerSupplyPhases": {
                        "type": "string",
                        "enum": [
                          "ONE_PHASE",
                          "THREE_PHASE"
                        ],
                        "description": "Whether the emergency supply is single-phase or three-phase.",
                        "example": "THREE_PHASE"
                      },
                      "netPrice": {
                        "type": "number",
                        "description": "Net price in euros.",
                        "example": 650
                      },
                      "emergencyPowerSwitchTimeMs": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Switch-over time in milliseconds.",
                        "example": 20
                      },
                      "emergencyPowerMaxMainsOperationCurrentA": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Maximum current in grid operation, in amperes.",
                        "example": 32
                      },
                      "emergencyPowerMaxOutputCurrentA": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Maximum output current in emergency operation, in amperes. Default 0.",
                        "example": 16
                      },
                      "emergencyPowerSwitchType": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "enum": [
                          "INTERN",
                          "EXTERN"
                        ],
                        "description": "Whether the inverter switches itself (`INTERN`) or an external device does (`EXTERN`).",
                        "example": "EXTERN"
                      },
                      "emergencyPowerManualSwitching": {
                        "type": [
                          "boolean",
                          "null"
                        ],
                        "description": "Whether switching is done by hand. Default `false`.",
                        "example": false
                      },
                      "recommended": {
                        "type": "boolean",
                        "description": "Marks the emergency power solution you offer for this inverter; others are alternatives.",
                        "example": true
                      },
                      "equipment": {
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "equipmentId": {
                              "type": "string",
                              "pattern": "^\\d+$",
                              "description": "The equipment material.",
                              "example": "37"
                            },
                            "netPrice": {
                              "type": "number",
                              "description": "Net price in euros.",
                              "example": 120
                            },
                            "optional": {
                              "type": "boolean",
                              "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                              "example": false
                            }
                          },
                          "required": [
                            "equipmentId",
                            "netPrice"
                          ]
                        },
                        "description": "Equipment for the emergency power solution."
                      }
                    },
                    "required": [
                      "name",
                      "emergencyPowerSupplyPhases",
                      "netPrice"
                    ]
                  },
                  "description": "Emergency power solutions for the inverter. Mark the one you offer with `recommended`."
                },
                "mpptStrings": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "trackerMaterialInverterMppTrackerId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "The MPP tracker the string is connected to (`mppTrackers[].id` of the inverter).",
                        "example": "141"
                      },
                      "roofProjectRoofConfigurationId": {
                        "type": "string",
                        "pattern": "^\\d+$",
                        "description": "The project roof the string's modules are on (`roofs[].id` of the project).",
                        "example": "301"
                      },
                      "parallelStringsCount": {
                        "type": "integer",
                        "description": "How many strings like this one are connected in parallel.",
                        "example": 1
                      },
                      "pvModulesCount": {
                        "type": "integer",
                        "description": "How many modules the string has (connected in series).",
                        "example": 11
                      }
                    },
                    "required": [
                      "trackerMaterialInverterMppTrackerId",
                      "roofProjectRoofConfigurationId",
                      "parallelStringsCount",
                      "pvModulesCount"
                    ]
                  },
                  "description": "Which modules are wired to which MPP tracker. The string plan (`GET /v1/offers/{id}/string-plan`) is drawn from this."
                }
              },
              "required": [
                "materialInverterId",
                "netPrice"
              ]
            },
            "description": "Inverters, with their equipment, emergency power and string layout."
          },
          "additionalCosts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "description": "Name of the line on the offer.",
                  "example": "Rabatt Sommeraktion"
                },
                "fixedPrice": {
                  "type": "number",
                  "description": "Net price in euros. A negative amount is a discount.",
                  "example": -300
                },
                "pricePerModule": {
                  "type": "number",
                  "description": "Net price in euros per module of the offer, added to `fixedPrice`. Default 0.",
                  "example": 0
                },
                "description": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "Text shown under the line.",
                  "example": "Gültig bis 31.08."
                },
                "optional": {
                  "type": "boolean",
                  "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it. Default `false`.",
                  "example": false
                },
                "salesTaxFreeEntitled": {
                  "type": "boolean",
                  "description": "Whether the line qualifies for the 0 % VAT rate for PV systems. Default `false`.",
                  "example": true
                }
              },
              "required": [
                "name",
                "fixedPrice"
              ]
            },
            "description": "Free cost lines. A negative price is a discount."
          }
        },
        "required": [
          "offer"
        ],
        "description": "A complete offer. Only `offer` with `projectId` and `associatedCompanyMemberId` is required; everything else can be added now or later, line by line, with the `/v1/offers/{id}/…` endpoints. Every line carries the net price it is sold for on this offer: the API does not take prices from the catalog. For the catalog price, use `net` from the material's `sellingPrices` entry for the project's site.",
        "example": {
          "offer": {
            "projectId": "41",
            "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
            "name": "Angebot PV 9,8 kWp mit Speicher",
            "validTo": "2026-10-31",
            "applySalesTaxFreeEntitled": true
          },
          "roofs": [
            {
              "projectRoofConfigurationId": "301",
              "active": true,
              "pvModulesCount": 22,
              "moduleConfigs": [
                {
                  "materialPvModuleId": "150",
                  "recommended": true,
                  "netPricePerModule": 98
                }
              ],
              "subconstruction": {
                "materialSubconstructionId": "9",
                "netPrice": 150,
                "pricePerModule": 32
              }
            }
          ],
          "inverters": [
            {
              "materialInverterId": "70",
              "netPrice": 1850,
              "mpptStrings": [
                {
                  "trackerMaterialInverterMppTrackerId": "141",
                  "roofProjectRoofConfigurationId": "301",
                  "pvModulesCount": 11,
                  "parallelStringsCount": 1
                },
                {
                  "trackerMaterialInverterMppTrackerId": "142",
                  "roofProjectRoofConfigurationId": "301",
                  "pvModulesCount": 11,
                  "parallelStringsCount": 1
                }
              ]
            }
          ],
          "batteries": [
            {
              "materialBatteryId": "88",
              "active": true,
              "netPrice": 4200
            }
          ],
          "services": [
            {
              "serviceId": "5",
              "fixedPrice": 450,
              "category": "ROOF"
            }
          ],
          "additionalCosts": [
            {
              "name": "Rabatt Sommeraktion",
              "fixedPrice": -300
            }
          ]
        }
      },
      "OfferStateInput": {
        "type": "object",
        "properties": {
          "state": {
            "type": "string",
            "enum": [
              "DRAFT",
              "ACCEPTED",
              "DECLINED",
              "PUBLISHED"
            ],
            "description": "The new state: `PUBLISHED`, `ACCEPTED` or `DECLINED`. `DRAFT` is only accepted for a draft.",
            "example": "PUBLISHED"
          }
        },
        "required": [
          "state"
        ],
        "description": "The state to move the offer to."
      },
      "OfferService": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The line's id.",
            "example": "1204"
          },
          "serviceId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The service.",
            "example": "5"
          },
          "fixedPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros.",
            "example": 450
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per module of the offer, added to `fixedPrice`.",
            "example": 0
          },
          "optional": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "`ROOF`, `ELECTRICIAN` or `OTHER`.",
            "example": "ROOF"
          },
          "orderPriority": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Position in the list; lower comes first.",
            "example": 1
          }
        },
        "required": [
          "id"
        ],
        "description": "A service on an offer."
      },
      "OfferServiceInput": {
        "type": "object",
        "properties": {
          "serviceId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The service (`GET /v1/services`).",
            "example": "5"
          },
          "fixedPrice": {
            "type": "number",
            "description": "Net price in euros.",
            "example": 450
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per module of the offer, added to `fixedPrice`. Default 0.",
            "example": 0
          },
          "optional": {
            "type": "boolean",
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "OTHER",
              "ELECTRICIAN",
              "ROOF"
            ],
            "description": "The section of the offer the service is listed in: `ROOF`, `ELECTRICIAN` or `OTHER`, the default.",
            "example": "ROOF"
          },
          "orderPriority": {
            "type": "integer",
            "description": "Position in the list; lower comes first. Default 0.",
            "example": 1
          }
        },
        "required": [
          "serviceId",
          "fixedPrice"
        ],
        "description": "A service line."
      },
      "OfferAdditionalCost": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The line's id.",
            "example": "530"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the line.",
            "example": "Rabatt Sommeraktion"
          },
          "fixedPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros; negative for a discount.",
            "example": -300
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per module of the offer, added to `fixedPrice`.",
            "example": 0
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Text shown under the line.",
            "example": "Gültig bis 31.08."
          },
          "optional": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          },
          "salesTaxFreeEntitled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the line qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          }
        },
        "required": [
          "id"
        ],
        "description": "A free cost line on an offer."
      },
      "OfferAdditionalCostInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Name of the line on the offer.",
            "example": "Rabatt Sommeraktion"
          },
          "fixedPrice": {
            "type": "number",
            "description": "Net price in euros. A negative amount is a discount.",
            "example": -300
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per module of the offer, added to `fixedPrice`. Default 0.",
            "example": 0
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Text shown under the line.",
            "example": "Gültig bis 31.08."
          },
          "optional": {
            "type": "boolean",
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the line qualifies for the 0 % VAT rate for PV systems. Default `false`.",
            "example": true
          }
        },
        "required": [
          "name",
          "fixedPrice"
        ],
        "description": "A free cost line. A negative `fixedPrice` makes it a discount."
      },
      "OfferBattery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The battery's material id; it identifies the line.",
            "example": "88"
          },
          "materialBatteryId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The battery.",
            "example": "88"
          },
          "netPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros.",
            "example": 4200
          },
          "active": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for the battery you offer; the others are alternatives.",
            "example": true
          }
        },
        "required": [
          "id"
        ],
        "description": "A battery on an offer."
      },
      "OfferBatteryInput": {
        "type": "object",
        "properties": {
          "materialBatteryId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The battery (`GET /v1/materials/batteries`). Each battery can be on an offer once; adding it again answers `409 already_exists`.",
            "example": "88"
          },
          "netPrice": {
            "type": "number",
            "description": "Net price in euros.",
            "example": 4200
          },
          "active": {
            "type": "boolean",
            "description": "`true` for the battery you offer. More batteries without it are alternatives the customer can choose instead. Default `false`.",
            "example": true
          }
        },
        "required": [
          "materialBatteryId",
          "netPrice"
        ],
        "description": "A battery for the offer."
      },
      "OfferWallbox": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The wallbox's material id; it identifies the line.",
            "example": "25"
          },
          "materialWallboxId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The wallbox.",
            "example": "25"
          },
          "netPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros.",
            "example": 790
          },
          "active": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for the wallbox you offer.",
            "example": true
          }
        },
        "required": [
          "id"
        ],
        "description": "A wallbox on an offer."
      },
      "OfferWallboxInput": {
        "type": "object",
        "properties": {
          "materialWallboxId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The wallbox (`GET /v1/materials/wallboxes`). Each wallbox can be on an offer once; adding it again answers `409 already_exists`.",
            "example": "25"
          },
          "netPrice": {
            "type": "number",
            "description": "Net price in euros.",
            "example": 790
          },
          "active": {
            "type": "boolean",
            "description": "`true` for the wallbox you offer. Default `false`.",
            "example": true
          }
        },
        "required": [
          "materialWallboxId",
          "netPrice"
        ],
        "description": "A wallbox (EV charger) for the offer."
      },
      "OfferMisc": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The line's id.",
            "example": "640"
          },
          "materialMiscId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The miscellaneous material.",
            "example": "8"
          },
          "netPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price per piece in euros.",
            "example": 3.5
          },
          "piecesCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many pieces.",
            "example": 40
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per module of the offer, on top.",
            "example": 0
          },
          "optional": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          }
        },
        "required": [
          "id"
        ],
        "description": "A line of miscellaneous material on an offer."
      },
      "OfferMiscInput": {
        "type": "object",
        "properties": {
          "materialMiscId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The miscellaneous material (`GET /v1/materials/misc`).",
            "example": "8"
          },
          "netPrice": {
            "type": "number",
            "description": "Net price per piece in euros.",
            "example": 3.5
          },
          "piecesCount": {
            "type": "integer",
            "description": "How many pieces.",
            "example": 40
          },
          "pricePerModule": {
            "type": "number",
            "description": "Net price in euros per module of the offer, on top. Default 0.",
            "example": 0
          },
          "optional": {
            "type": "boolean",
            "description": "`true` for an optional extra: shown on the offer, but only part of the price when the customer picks it.",
            "example": false
          }
        },
        "required": [
          "materialMiscId",
          "netPrice",
          "piecesCount"
        ],
        "description": "A line of miscellaneous material, such as cable or small parts."
      },
      "Customer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The customer's id.",
            "example": "77"
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ],
            "description": "First name.",
            "example": "Anna"
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Last name.",
            "example": "Müller"
          },
          "fullName": {
            "type": [
              "string",
              "null"
            ],
            "description": "First and last name in one string, for display.",
            "example": "Anna Müller"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address.",
            "example": "anna.mueller@example.com"
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 89 1234567"
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "description": "The customer's postal address."
              }
            ]
          },
          "projectsCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many projects the customer has.",
            "example": 1
          },
          "offersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many offers there are across all of the customer's projects.",
            "example": 2
          },
          "createdAt": {
            "type": "string",
            "description": "When the customer was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "A customer of your company: the person you plan a PV system for and make offers to."
      },
      "CustomerList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Customer"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "CustomerInput": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "First name. Required when creating a customer.",
            "example": "Anna"
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 99,
            "description": "Last name.",
            "example": "Müller"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 99,
            "description": "Email address. Offers and booking mails go here.",
            "example": "anna.mueller@example.com"
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "Phone number, at most 20 characters.",
            "example": "+49 89 1234567"
          },
          "gender": {
            "type": "string",
            "enum": [
              "MALE",
              "FEMALE",
              "UNSPECIFIED"
            ],
            "description": "Sets the salutation in documents and emails: `MALE` (Herr), `FEMALE` (Frau) or `UNSPECIFIED` (no salutation, the default).",
            "example": "FEMALE"
          },
          "address": {
            "type": "object",
            "properties": {
              "zip": {
                "type": "string",
                "description": "Postal code.",
                "example": "80331"
              },
              "city": {
                "type": "string",
                "description": "Town or city.",
                "example": "München"
              },
              "street": {
                "type": "string",
                "description": "Street name.",
                "example": "Lindenstraße"
              },
              "streetNumber": {
                "type": "string",
                "description": "House number.",
                "example": "12"
              }
            },
            "description": "Postal address. Send only the parts you want to set."
          }
        },
        "required": [
          "firstName"
        ],
        "description": "A customer to create (`POST`) or change (`PATCH`). When changing, send only the fields to change; `null` clears a field.",
        "example": {
          "firstName": "Anna",
          "lastName": "Müller",
          "email": "anna.mueller@example.com",
          "phoneNumber": "+49 89 1234567",
          "gender": "FEMALE",
          "address": {
            "street": "Lindenstraße",
            "streetNumber": "12",
            "zip": "80331",
            "city": "München"
          }
        }
      },
      "CustomerUpdate": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "First name. Required when creating a customer.",
            "example": "Anna"
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 99,
            "description": "Last name.",
            "example": "Müller"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 99,
            "description": "Email address. Offers and booking mails go here.",
            "example": "anna.mueller@example.com"
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 20,
            "description": "Phone number, at most 20 characters.",
            "example": "+49 89 1234567"
          },
          "gender": {
            "type": "string",
            "enum": [
              "MALE",
              "FEMALE",
              "UNSPECIFIED"
            ],
            "description": "Sets the salutation in documents and emails: `MALE` (Herr), `FEMALE` (Frau) or `UNSPECIFIED` (no salutation, the default).",
            "example": "FEMALE"
          },
          "address": {
            "type": "object",
            "properties": {
              "zip": {
                "type": "string",
                "description": "Postal code.",
                "example": "80331"
              },
              "city": {
                "type": "string",
                "description": "Town or city.",
                "example": "München"
              },
              "street": {
                "type": "string",
                "description": "Street name.",
                "example": "Lindenstraße"
              },
              "streetNumber": {
                "type": "string",
                "description": "House number.",
                "example": "12"
              }
            },
            "description": "Postal address. Send only the parts you want to set."
          }
        },
        "description": "Changes to a customer. Send only the fields to change; `null` clears a field.",
        "example": {
          "email": "anna@mueller-family.example"
        }
      },
      "OfferRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The offer request's id.",
            "example": "501"
          },
          "state": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the request stands. Waiting for the customer: `REQUIRED_DATA_MISSING`, `REQUIRED_DATA_INSUFFICIENT`, `REQUIRED_IMAGES_MISSING`, `REQUIRED_IMAGES_INSUFFICIENT`. Waiting for you: `REQUIRED_DATA_UPLOADED`, `REQUIRED_IMAGES_UPLOADED`. Done: `REVIEW_SUCCESSFUL` (ready for an offer), `REQUEST_NO_RESPONSE` (the customer stopped answering), `REQUEST_REJECTED`.",
            "example": "REVIEW_SUCCESSFUL"
          },
          "customer": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "firstName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First name.",
                "example": "Anna"
              },
              "lastName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Last name.",
                "example": "Müller"
              },
              "fullName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First and last name in one string.",
                "example": "Anna Müller"
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Email address.",
                "example": "anna.mueller@example.com"
              },
              "phoneNumber": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Phone number.",
                "example": "+49 89 1234567"
              },
              "address": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Address"
                  },
                  {
                    "description": "The customer's postal address."
                  }
                ]
              }
            },
            "description": "The customer who made the request."
          },
          "projectId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The project the request belongs to.",
            "example": "41"
          },
          "projectName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of that project.",
            "example": "PV Müller Satteldach"
          },
          "companyMemberName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The team member handling the request.",
            "example": "Max Mustermann"
          },
          "offersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Offers made for the request so far.",
            "example": 2
          },
          "indicationOffersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many of them are indication offers.",
            "example": 1
          },
          "detailedOffersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How many of them are detailed offers.",
            "example": 1
          },
          "createdAt": {
            "type": "string",
            "description": "When the request came in (ISO 8601).",
            "example": "2026-06-28T18:04:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "A request (Anfrage) from a customer, usually through your configurator. You review it, then make offers for it."
      },
      "OfferRequestList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OfferRequest"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "PurchasePrice": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "pricePerPiece": {
            "type": [
              "number",
              "null"
            ],
            "description": "What you pay per piece, net, in euros.",
            "example": 98.5
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "What you pay per PV module of an offer, net, in euros, on top of the price per piece. Misc materials and mounting systems only; `null` for the other types.",
            "example": 0
          },
          "vendorName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The vendor you buy from.",
            "example": "Solar-Großhandel Schmidt"
          },
          "vendorLink": {
            "type": [
              "string",
              "null"
            ],
            "description": "Link to the product at the vendor.",
            "example": "https://shop.example.com/aiko-neostar-445"
          },
          "vendorArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The vendor's own article number for the product, as on their price list. The price-list import matches by it.",
            "example": "A09402"
          }
        },
        "description": "What you pay for the material; `null` when no purchase price is recorded."
      },
      "SellingPrice": {
        "type": "object",
        "properties": {
          "siteId": {
            "type": "string",
            "description": "The company site (`GET /v1/company/sites`).",
            "example": "1"
          },
          "siteName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The company site's name.",
            "example": "München"
          },
          "net": {
            "type": [
              "number",
              "null"
            ],
            "description": "Selling price per piece at this site, net, in euros: the purchase price plus the site's margin for this kind of product. `null` while there is no purchase price.",
            "example": 117.22
          },
          "gross": {
            "type": [
              "number",
              "null"
            ],
            "description": "The same including VAT. Equal to `net` for products that qualify for the 0 % VAT rate for PV systems.",
            "example": 117.22
          },
          "netPerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Selling price per PV module of an offer, net. Misc materials and mounting systems only.",
            "example": 0
          },
          "grossPerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "The same including VAT. Misc materials and mounting systems only.",
            "example": 0
          }
        },
        "required": [
          "siteId"
        ],
        "description": "What the material sells for at one of your company sites (Standorte), worked out from the purchase price and the site's margin."
      },
      "MppTracker": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The tracker's id. Offers refer to it in `mpptCircuitTypes` and `mpptStrings`. Send it back in `mppTrackers` to keep the tracker when you change the inverter's trackers.",
            "example": "141"
          },
          "inputsCount": {
            "type": "integer",
            "description": "How many string inputs the tracker has.",
            "example": 2
          },
          "maxInputCurrentA": {
            "type": "number",
            "description": "Highest input current in amperes.",
            "example": 13.5
          },
          "maxShortCircuitCurrentA": {
            "type": "number",
            "description": "Highest short-circuit current in amperes.",
            "example": 16.9
          },
          "maxInputPowerKw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Highest input power in kW; `null` when not recorded.",
            "example": 7.5
          }
        },
        "required": [
          "id",
          "inputsCount",
          "maxInputCurrentA",
          "maxShortCircuitCurrentA",
          "maxInputPowerKw"
        ],
        "description": "One MPP tracker of an inverter."
      },
      "Material": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The material's id. Ids are unique per material type only: a PV module and a battery can have the same id.",
            "example": "150"
          },
          "type": {
            "type": "string",
            "description": "The material type: `pv_module`, `battery`, `inverter`, `wallbox`, `equipment`, `misc`, `subconstruction` or `emergency_power`. It decides what `specs` holds.",
            "example": "pv_module"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product name as it appears on offers.",
            "example": "Aiko Neostar 2S 445W"
          },
          "manufacturerId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer.",
            "example": "12"
          },
          "manufacturerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's name.",
            "example": "Aiko"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number. You can use it instead of the id: list, update or delete with `?manufacturerArticleNumber=` on the type's collection. It is not unique: two manufacturers can use the same number, and a product entered once per size (a stackable battery in 5, 10 and 15 kWh) carries it on every row. Such a request answers `409 ambiguous_key` with the ids that match.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Unique across all material types among active products; look a product up by it with `GET /v1/materials?internalArticleNumber=`.",
            "example": "PV-0042"
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePrice"
          },
          "sellingPrices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SellingPrice"
            },
            "description": "What the material sells for, one entry per company site. These are the prices to put on offer lines."
          },
          "mppTrackers": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/MppTracker"
            },
            "description": "Inverters only: the inverter's MPP trackers, in order. `null` for the other types."
          },
          "archived": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Archived materials stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the material was created (ISO 8601).",
            "example": "2026-06-12T08:00:00+00:00"
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the material was last changed (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          },
          "specs": {
            "type": "object",
            "additionalProperties": {},
            "description": "Technical data, depending on `type`. `pv_module`: `nominalPowerW`, `moduleMaterial`, `uMppV`, `iMppA`. `battery`: `capacityKwh`, `powerKw`. `inverter`: `mpptCount`, `acNominalPowerKw`, `batteryConfigType`, `emergencyPowersCount`. `wallbox`: `chargingPower`. `subconstruction`: `roofType`. `equipment`, `misc` and `emergency_power`: none.",
            "example": {
              "nominalPowerW": 445,
              "moduleMaterial": "GLASS_GLASS",
              "uMppV": 33.6,
              "iMppA": 13.2
            }
          }
        },
        "required": [
          "id",
          "type",
          "sellingPrices",
          "specs"
        ],
        "description": "A product in your catalog. Every material type has this shape; the technical data is in `specs`."
      },
      "MaterialList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Material"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "PurchasePriceImportCandidate": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "type": {
            "type": "string",
            "example": "inverter"
          },
          "id": {
            "type": "string",
            "example": "70"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "type",
          "id",
          "name"
        ]
      },
      "PurchasePriceImportRowResult": {
        "type": "object",
        "properties": {
          "index": {
            "type": "integer",
            "description": "Position of the row in your request, starting at 0.",
            "example": 0
          },
          "status": {
            "type": "string",
            "enum": [
              "updated",
              "matched",
              "unmatched",
              "ambiguous",
              "error"
            ],
            "description": "`updated`: purchase price written. `matched`: would be written (dry run). `unmatched`: no active product carries any of the row's keys — record the vendor number on the product once and it will match next time. `ambiguous`: several products match; send `manufacturerId` or fix the catalog. `error`: the write failed, see `message`."
          },
          "matchedBy": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "vendorArticleNumber",
              "manufacturerArticleNumber"
            ]
          },
          "material": {
            "$ref": "#/components/schemas/PurchasePriceImportCandidate"
          },
          "purchasePriceId": {
            "type": [
              "string",
              "null"
            ]
          },
          "candidates": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/PurchasePriceImportCandidate"
                },
                {
                  "type": "object"
                }
              ]
            },
            "description": "With `ambiguous`: the products that matched."
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "index",
          "status",
          "matchedBy",
          "material",
          "purchasePriceId"
        ]
      },
      "PurchasePriceImportResult": {
        "type": "object",
        "properties": {
          "dryRun": {
            "type": "boolean"
          },
          "vendorName": {
            "type": "string"
          },
          "summary": {
            "type": "object",
            "properties": {
              "updated": {
                "type": "integer"
              },
              "matched": {
                "type": "integer"
              },
              "unmatched": {
                "type": "integer"
              },
              "ambiguous": {
                "type": "integer"
              },
              "error": {
                "type": "integer"
              }
            },
            "required": [
              "updated",
              "matched",
              "unmatched",
              "ambiguous",
              "error"
            ]
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PurchasePriceImportRowResult"
            }
          }
        },
        "required": [
          "dryRun",
          "vendorName",
          "summary",
          "rows"
        ]
      },
      "PurchasePriceImportRow": {
        "type": "object",
        "properties": {
          "vendorArticleNumber": {
            "type": "string",
            "description": "The vendor's article number for the product. Tried first: it matches the product whose purchase price from this vendor carries that number.",
            "example": "A09402"
          },
          "manufacturerArticleNumber": {
            "type": "string",
            "description": "Tried when the vendor article number is not sent or not known yet.",
            "example": "A-MAH54Mb-445"
          },
          "manufacturerId": {
            "type": "string",
            "description": "Only consider this manufacturer's products when matching by manufacturer article number. Needed when two manufacturers use the same number.",
            "example": "12"
          },
          "pricePerPiece": {
            "type": "number",
            "description": "Net purchase price per piece in euros, from the vendor's list.",
            "example": 98.5
          },
          "vendorLink": {
            "type": [
              "string",
              "null"
            ],
            "description": "Link to the vendor's product page. Omit to keep the link the product's price from this vendor already has; send null to clear it."
          }
        },
        "required": [
          "pricePerPiece"
        ]
      },
      "PurchasePriceImport": {
        "type": "object",
        "properties": {
          "vendorName": {
            "type": "string",
            "minLength": 1,
            "description": "The vendor the price list comes from, matched ignoring case and surrounding whitespace. A product's purchase price from this vendor is updated; a product without one gets one.",
            "example": "Solar-Großhandel Schmidt"
          },
          "dryRun": {
            "type": "boolean",
            "default": false,
            "description": "`true` to only see how each row would match, without writing anything.",
            "example": false
          },
          "rows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PurchasePriceImportRow"
            },
            "minItems": 1,
            "maxItems": 200,
            "description": "The prices, at most 200 per call. Send longer lists in several calls."
          }
        },
        "required": [
          "vendorName",
          "rows"
        ],
        "description": "A vendor's price list.",
        "example": {
          "vendorName": "Solar-Großhandel Schmidt",
          "dryRun": true,
          "rows": [
            {
              "vendorArticleNumber": "A09402",
              "manufacturerArticleNumber": "A-MAH54Mb-445",
              "pricePerPiece": 98.5
            },
            {
              "vendorArticleNumber": "W-11873",
              "manufacturerArticleNumber": "SUN2000-10KTL-M1",
              "pricePerPiece": 1499
            }
          ]
        }
      },
      "DimensionsInput": {
        "type": "object",
        "properties": {
          "widthMm": {
            "type": [
              "number",
              "null"
            ],
            "description": "Width in millimetres.",
            "example": 1134
          },
          "heightMm": {
            "type": [
              "number",
              "null"
            ],
            "description": "Height in millimetres.",
            "example": 1762
          },
          "depthMm": {
            "type": [
              "number",
              "null"
            ],
            "description": "Depth in millimetres.",
            "example": 30
          }
        },
        "description": "Size of the product in millimetres."
      },
      "PurchasePriceInput": {
        "type": "object",
        "properties": {
          "pricePerPiece": {
            "type": "number",
            "description": "Net purchase price per piece in euros.",
            "example": 98.5
          },
          "vendorName": {
            "type": "string",
            "description": "Who you buy from. Each vendor has one price per product; the name is matched ignoring case and surrounding spaces, so write it as the planner shows it.",
            "example": "Solar-Großhandel Schmidt"
          },
          "vendorLink": {
            "type": [
              "string",
              "null"
            ],
            "description": "Link to the product at the vendor. Leave it out to keep the current link; `null` clears it.",
            "example": "https://shop.example.com/aiko-neostar-445"
          },
          "vendorArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The vendor's own article number, as on their price list. Leave it out to keep the current one; `null` clears it. A number other than the one recorded for this vendor counts as another article and gets a price of its own.",
            "example": "A09402"
          }
        },
        "required": [
          "pricePerPiece",
          "vendorName"
        ],
        "description": "What you pay for the product, and at which vendor. If the product already has a price from this vendor, that price is updated; otherwise one is added. Either way it becomes the price the catalog uses."
      },
      "PvModuleInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Aiko Neostar 2S 445W"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "nominalPowerW": {
            "type": "number",
            "description": "Nominal power in watts (STC).",
            "example": 445
          },
          "uMppV": {
            "type": "number",
            "description": "Voltage at the maximum power point, in volts.",
            "example": 33.6
          },
          "iMppA": {
            "type": "number",
            "description": "Current at the maximum power point, in amperes.",
            "example": 13.25
          },
          "noLoadVoltageV": {
            "type": "number",
            "description": "Open-circuit voltage (Uoc) in volts.",
            "example": 40.3
          },
          "shortCircuitCurrentI": {
            "type": "number",
            "description": "Short-circuit current (Isc) in amperes.",
            "example": 14.03
          },
          "moduleMaterial": {
            "type": "string",
            "enum": [
              "GLASS_GLASS",
              "GLASS_FOIL"
            ],
            "description": "`GLASS_GLASS` or `GLASS_FOIL` (glass front, foil back).",
            "example": "GLASS_GLASS"
          },
          "plug": {
            "type": "string",
            "enum": [
              "MC4_STAEUBLI",
              "MC4_STAEUBLI_COMPATIBLE"
            ],
            "description": "Connector: original Stäubli MC4, or compatible.",
            "example": "MC4_STAEUBLI"
          },
          "fullBlack": {
            "type": "boolean",
            "description": "Whether the module is all black.",
            "example": true
          },
          "uOcCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the open-circuit voltage, in % per kelvin.",
            "example": -0.25
          },
          "iScCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the short-circuit current, in % per kelvin.",
            "example": 0.05
          },
          "pCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the power, in % per kelvin.",
            "example": -0.26
          },
          "performanceGuaranteeYears": {
            "type": "integer",
            "description": "Performance guarantee in years.",
            "example": 30
          },
          "bifacialityFactorPercent": {
            "type": [
              "number",
              "null"
            ],
            "description": "Bifaciality factor in percent, for bifacial modules.",
            "example": 80
          }
        },
        "required": [
          "name",
          "manufacturerId",
          "nominalPowerW",
          "uMppV",
          "iMppA",
          "noLoadVoltageV",
          "shortCircuitCurrentI",
          "moduleMaterial",
          "plug",
          "fullBlack",
          "uOcCoefficientPercentPerKelvin",
          "iScCoefficientPercentPerKelvin",
          "pCoefficientPercentPerKelvin",
          "performanceGuaranteeYears"
        ],
        "description": "A PV module. The electrical data is needed to plan the strings, so it is required."
      },
      "PvModuleUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Aiko Neostar 2S 445W"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "nominalPowerW": {
            "type": "number",
            "description": "Nominal power in watts (STC).",
            "example": 445
          },
          "uMppV": {
            "type": "number",
            "description": "Voltage at the maximum power point, in volts.",
            "example": 33.6
          },
          "iMppA": {
            "type": "number",
            "description": "Current at the maximum power point, in amperes.",
            "example": 13.25
          },
          "noLoadVoltageV": {
            "type": "number",
            "description": "Open-circuit voltage (Uoc) in volts.",
            "example": 40.3
          },
          "shortCircuitCurrentI": {
            "type": "number",
            "description": "Short-circuit current (Isc) in amperes.",
            "example": 14.03
          },
          "moduleMaterial": {
            "type": "string",
            "enum": [
              "GLASS_GLASS",
              "GLASS_FOIL"
            ],
            "description": "`GLASS_GLASS` or `GLASS_FOIL` (glass front, foil back).",
            "example": "GLASS_GLASS"
          },
          "plug": {
            "type": "string",
            "enum": [
              "MC4_STAEUBLI",
              "MC4_STAEUBLI_COMPATIBLE"
            ],
            "description": "Connector: original Stäubli MC4, or compatible.",
            "example": "MC4_STAEUBLI"
          },
          "fullBlack": {
            "type": "boolean",
            "description": "Whether the module is all black.",
            "example": true
          },
          "uOcCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the open-circuit voltage, in % per kelvin.",
            "example": -0.25
          },
          "iScCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the short-circuit current, in % per kelvin.",
            "example": 0.05
          },
          "pCoefficientPercentPerKelvin": {
            "type": "number",
            "description": "Temperature coefficient of the power, in % per kelvin.",
            "example": -0.26
          },
          "performanceGuaranteeYears": {
            "type": "integer",
            "description": "Performance guarantee in years.",
            "example": 30
          },
          "bifacialityFactorPercent": {
            "type": [
              "number",
              "null"
            ],
            "description": "Bifaciality factor in percent, for bifacial modules.",
            "example": 80
          }
        },
        "description": "A PV module. The electrical data is needed to plan the strings, so it is required. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "BatteryInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "BYD Battery-Box Premium HVS 10.2"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "capacityKwh": {
            "type": "number",
            "description": "Usable capacity in kWh.",
            "example": 10.24
          },
          "powerKw": {
            "type": "number",
            "description": "Nominal power in kW.",
            "example": 10.24
          },
          "linkType": {
            "type": "string",
            "enum": [
              "AC",
              "DC"
            ],
            "description": "How the battery is connected: `AC` or `DC`.",
            "example": "DC"
          },
          "chemicalType": {
            "type": "string",
            "enum": [
              "LFP",
              "NMC"
            ],
            "description": "Cell chemistry: `LFP` or `NMC`.",
            "example": "LFP"
          },
          "montageFloor": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Floor mounting: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "montageWall": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Wall mounting: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "NOT_INCLUDED"
          },
          "nominalVoltageV": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal voltage in volts.",
            "example": 409.6
          },
          "nominalChargeCurrentA": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal charge current in amperes.",
            "example": 25
          },
          "nominalDischargeCurrentA": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal discharge current in amperes.",
            "example": 25
          },
          "nominalDischargePowerKw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal discharge power in kW.",
            "example": 10.24
          }
        },
        "required": [
          "name",
          "manufacturerId",
          "capacityKwh",
          "powerKw",
          "linkType",
          "chemicalType",
          "montageFloor",
          "montageWall"
        ],
        "description": "A battery storage product."
      },
      "BatteryUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "BYD Battery-Box Premium HVS 10.2"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "capacityKwh": {
            "type": "number",
            "description": "Usable capacity in kWh.",
            "example": 10.24
          },
          "powerKw": {
            "type": "number",
            "description": "Nominal power in kW.",
            "example": 10.24
          },
          "linkType": {
            "type": "string",
            "enum": [
              "AC",
              "DC"
            ],
            "description": "How the battery is connected: `AC` or `DC`.",
            "example": "DC"
          },
          "chemicalType": {
            "type": "string",
            "enum": [
              "LFP",
              "NMC"
            ],
            "description": "Cell chemistry: `LFP` or `NMC`.",
            "example": "LFP"
          },
          "montageFloor": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Floor mounting: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "montageWall": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Wall mounting: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "NOT_INCLUDED"
          },
          "nominalVoltageV": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal voltage in volts.",
            "example": 409.6
          },
          "nominalChargeCurrentA": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal charge current in amperes.",
            "example": 25
          },
          "nominalDischargeCurrentA": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal discharge current in amperes.",
            "example": 25
          },
          "nominalDischargePowerKw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal discharge power in kW.",
            "example": 10.24
          }
        },
        "description": "A battery storage product. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "MppTrackerInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The id of an existing tracker to change (`mppTrackers[].id` of the inverter). Leave it out to add a new tracker.",
            "example": "141"
          },
          "inputsCount": {
            "type": "integer",
            "minimum": 1,
            "description": "How many string inputs the tracker has, at least 1.",
            "example": 2
          },
          "maxInputCurrentA": {
            "type": "number",
            "minimum": 0,
            "description": "Highest input current in amperes.",
            "example": 13.5
          },
          "maxShortCircuitCurrentA": {
            "type": "number",
            "minimum": 0,
            "description": "Highest short-circuit current in amperes.",
            "example": 16.9
          },
          "maxInputPowerKw": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Highest input power in kW. Leave it out if the datasheet does not give it.",
            "example": 7.5
          }
        },
        "required": [
          "inputsCount",
          "maxInputCurrentA",
          "maxShortCircuitCurrentA"
        ],
        "description": "One MPP tracker of the inverter."
      },
      "InverterInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Huawei SUN2000-10KTL-M1"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "dcMinInputVoltageV": {
            "type": "number",
            "description": "Lowest DC input voltage in volts.",
            "example": 140
          },
          "dcMaxInputVoltageV": {
            "type": "number",
            "description": "Highest DC input voltage in volts.",
            "example": 1100
          },
          "dcMaxPowerKw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Highest DC input power in kW.",
            "example": 15
          },
          "dcOverVoltageProtection1": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 1 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "dcOverVoltageProtection2": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 2 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "dcOverVoltageProtection3": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 3 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection1": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 1 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection2": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 2 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection3": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 3 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acNominalPowerKw": {
            "type": "number",
            "description": "Nominal AC power in kW.",
            "example": 10
          },
          "acMaxPowerKw": {
            "type": "number",
            "description": "Highest AC power in kW.",
            "example": 11
          },
          "acRatedCurrentI": {
            "type": "number",
            "description": "Rated AC current in amperes.",
            "example": 16.9
          },
          "acNominalVoltageV": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal AC voltage in volts.",
            "example": 400
          },
          "acSupplyPhasesType": {
            "type": "string",
            "enum": [
              "ONE_PHASE",
              "THREE_PHASE"
            ],
            "description": "`ONE_PHASE` or `THREE_PHASE`.",
            "example": "THREE_PHASE"
          },
          "acZnasType": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Central grid and system protection (NA-Schutz): `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "ethernet": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Ethernet: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "wlan": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "WLAN: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "rse": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Ripple control receiver connection (Rundsteuerempfänger): `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "OPTIONAL"
          },
          "smartgridReady": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Smart-grid readiness: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "batteryConfigurationType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Battery connection: `INCLUDED`, `OPTIONAL` or `NOT_INCLUDED`.",
            "example": "OPTIONAL"
          },
          "mppTrackers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MppTrackerInput"
            },
            "description": "The inverter's MPP trackers. Offers wire the module strings to them, so an inverter without trackers cannot be planned on an offer. Send the complete list: a tracker with an `id` is changed, one without is added, and the inverter's other trackers are deleted. A tracker that an offer uses cannot be deleted (`409 in_use`). Leave the list out to keep the trackers as they are. A tracker you add is not added to offers that have the inverter already.",
            "example": [
              {
                "inputsCount": 2,
                "maxInputCurrentA": 13.5,
                "maxShortCircuitCurrentA": 16.9,
                "maxInputPowerKw": 7.5
              },
              {
                "inputsCount": 2,
                "maxInputCurrentA": 13.5,
                "maxShortCircuitCurrentA": 16.9,
                "maxInputPowerKw": 7.5
              }
            ]
          }
        },
        "required": [
          "name",
          "manufacturerId",
          "dcMinInputVoltageV",
          "dcMaxInputVoltageV",
          "dcOverVoltageProtection1",
          "dcOverVoltageProtection2",
          "dcOverVoltageProtection3",
          "acOverVoltageProtection1",
          "acOverVoltageProtection2",
          "acOverVoltageProtection3",
          "acNominalPowerKw",
          "acMaxPowerKw",
          "acRatedCurrentI",
          "acSupplyPhasesType",
          "acZnasType",
          "ethernet",
          "wlan",
          "rse",
          "smartgridReady"
        ],
        "description": "An inverter, with its MPP trackers. When the inverter is put on an offer, each tracker comes with it."
      },
      "InverterUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Huawei SUN2000-10KTL-M1"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "dcMinInputVoltageV": {
            "type": "number",
            "description": "Lowest DC input voltage in volts.",
            "example": 140
          },
          "dcMaxInputVoltageV": {
            "type": "number",
            "description": "Highest DC input voltage in volts.",
            "example": 1100
          },
          "dcMaxPowerKw": {
            "type": [
              "number",
              "null"
            ],
            "description": "Highest DC input power in kW.",
            "example": 15
          },
          "dcOverVoltageProtection1": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 1 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "dcOverVoltageProtection2": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 2 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "dcOverVoltageProtection3": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 3 surge protection on the DC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection1": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 1 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection2": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 2 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acOverVoltageProtection3": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NO_PROTECTION",
              "OPTIONAL"
            ],
            "description": "Type 3 surge protection on the AC side: `INCLUDED`, `OPTIONAL` or `NO_PROTECTION`.",
            "example": "INCLUDED"
          },
          "acNominalPowerKw": {
            "type": "number",
            "description": "Nominal AC power in kW.",
            "example": 10
          },
          "acMaxPowerKw": {
            "type": "number",
            "description": "Highest AC power in kW.",
            "example": 11
          },
          "acRatedCurrentI": {
            "type": "number",
            "description": "Rated AC current in amperes.",
            "example": 16.9
          },
          "acNominalVoltageV": {
            "type": [
              "number",
              "null"
            ],
            "description": "Nominal AC voltage in volts.",
            "example": 400
          },
          "acSupplyPhasesType": {
            "type": "string",
            "enum": [
              "ONE_PHASE",
              "THREE_PHASE"
            ],
            "description": "`ONE_PHASE` or `THREE_PHASE`.",
            "example": "THREE_PHASE"
          },
          "acZnasType": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Central grid and system protection (NA-Schutz): `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "ethernet": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Ethernet: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "wlan": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "WLAN: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "rse": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Ripple control receiver connection (Rundsteuerempfänger): `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "OPTIONAL"
          },
          "smartgridReady": {
            "type": "string",
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Smart-grid readiness: `INCLUDED`, `OPTIONAL` (available as an extra) or `NOT_INCLUDED`.",
            "example": "INCLUDED"
          },
          "batteryConfigurationType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "INCLUDED",
              "NOT_INCLUDED",
              "OPTIONAL"
            ],
            "description": "Battery connection: `INCLUDED`, `OPTIONAL` or `NOT_INCLUDED`.",
            "example": "OPTIONAL"
          },
          "mppTrackers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MppTrackerInput"
            },
            "description": "The inverter's MPP trackers. Offers wire the module strings to them, so an inverter without trackers cannot be planned on an offer. Send the complete list: a tracker with an `id` is changed, one without is added, and the inverter's other trackers are deleted. A tracker that an offer uses cannot be deleted (`409 in_use`). Leave the list out to keep the trackers as they are. A tracker you add is not added to offers that have the inverter already.",
            "example": [
              {
                "inputsCount": 2,
                "maxInputCurrentA": 13.5,
                "maxShortCircuitCurrentA": 16.9,
                "maxInputPowerKw": 7.5
              },
              {
                "inputsCount": 2,
                "maxInputCurrentA": 13.5,
                "maxShortCircuitCurrentA": 16.9,
                "maxInputPowerKw": 7.5
              }
            ]
          }
        },
        "description": "An inverter, with its MPP trackers. When the inverter is put on an offer, each tracker comes with it. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "WallboxInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "go-e Charger Gemini 11 kW"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "chargingPower": {
            "type": "string",
            "enum": [
              "POWER_11_KW",
              "POWER_22_KW"
            ],
            "description": "Charging power: `POWER_11_KW` or `POWER_22_KW`.",
            "example": "POWER_11_KW"
          },
          "meterIntegrated": {
            "type": "boolean",
            "description": "Whether a meter is built in.",
            "example": true
          },
          "rfidIntegrated": {
            "type": "boolean",
            "description": "Whether an RFID reader is built in.",
            "example": true
          },
          "dcFaultCurrentDetection": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether DC fault current detection is built in.",
            "example": true
          },
          "networkEthernet": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Ethernet connection.",
            "example": false
          },
          "networkWlan": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "WLAN connection.",
            "example": true
          },
          "networkDirectConnection": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Direct connection to the inverter.",
            "example": false
          }
        },
        "required": [
          "name",
          "manufacturerId",
          "chargingPower",
          "meterIntegrated",
          "rfidIntegrated"
        ],
        "description": "A wallbox (EV charger)."
      },
      "WallboxUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "go-e Charger Gemini 11 kW"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          "chargingPower": {
            "type": "string",
            "enum": [
              "POWER_11_KW",
              "POWER_22_KW"
            ],
            "description": "Charging power: `POWER_11_KW` or `POWER_22_KW`.",
            "example": "POWER_11_KW"
          },
          "meterIntegrated": {
            "type": "boolean",
            "description": "Whether a meter is built in.",
            "example": true
          },
          "rfidIntegrated": {
            "type": "boolean",
            "description": "Whether an RFID reader is built in.",
            "example": true
          },
          "dcFaultCurrentDetection": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether DC fault current detection is built in.",
            "example": true
          },
          "networkEthernet": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Ethernet connection.",
            "example": false
          },
          "networkWlan": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "WLAN connection.",
            "example": true
          },
          "networkDirectConnection": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Direct connection to the inverter.",
            "example": false
          }
        },
        "description": "A wallbox (EV charger). Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "EquipmentInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Optimierer SUN2000-450W-P2"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          }
        },
        "required": [
          "name",
          "manufacturerId"
        ],
        "description": "Equipment that goes with modules, inverters, batteries, wallboxes or a mounting system on an offer."
      },
      "EquipmentUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Optimierer SUN2000-450W-P2"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          }
        },
        "description": "Equipment that goes with modules, inverters, batteries, wallboxes or a mounting system on an offer. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "PurchasePriceWithModuleInput": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PurchasePriceInput"
          },
          {
            "type": "object",
            "properties": {
              "pricePerModule": {
                "type": "number",
                "description": "What you pay per PV module of an offer, net, in euros, on top of the price per piece. Leave it out to keep the current one (0 for a new vendor).",
                "example": 2.5
              }
            }
          }
        ],
        "description": "What you pay for the product, per piece and per PV module of an offer, and at which vendor. If the product already has a price from this vendor, that price is updated; otherwise one is added. Either way it becomes the price the catalog uses."
      },
      "MiscMaterialInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "PV-Kabel 6 mm², pro Meter"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceWithModuleInput"
          },
          "includedInOffer": {
            "type": "boolean",
            "description": "Put the material on new detailed offers automatically.",
            "example": true
          },
          "includedInIndicationOffer": {
            "type": "boolean",
            "description": "Put the material on new indication offers automatically.",
            "example": false
          },
          "optional": {
            "type": "boolean",
            "description": "When put on an offer, as an optional extra the customer can choose.",
            "example": false
          }
        },
        "required": [
          "name"
        ],
        "description": "A miscellaneous material, such as cable or small parts. It needs no manufacturer."
      },
      "MiscMaterialUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "PV-Kabel 6 mm², pro Meter"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceWithModuleInput"
          },
          "includedInOffer": {
            "type": "boolean",
            "description": "Put the material on new detailed offers automatically.",
            "example": true
          },
          "includedInIndicationOffer": {
            "type": "boolean",
            "description": "Put the material on new indication offers automatically.",
            "example": false
          },
          "optional": {
            "type": "boolean",
            "description": "When put on an offer, as an optional extra the customer can choose.",
            "example": false
          }
        },
        "description": "A miscellaneous material, such as cable or small parts. It needs no manufacturer. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "SubconstructionInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "K2 SingleRail 36"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceWithModuleInput"
          },
          "roofType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "TILED_ROOF",
              "BITUMEN_ROOF",
              "TRAPEZOIDAL_SHEET_METAL_ROOF",
              "BEADED_PLATE_ROOF",
              "FACADE",
              "OPEN_FIELD"
            ],
            "description": "The roof type the mounting system is for.",
            "example": "TILED_ROOF"
          }
        },
        "required": [
          "name",
          "manufacturerId"
        ],
        "description": "A mounting system (Unterkonstruktion)."
      },
      "SubconstructionUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "K2 SingleRail 36"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceWithModuleInput"
          },
          "roofType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "TILED_ROOF",
              "BITUMEN_ROOF",
              "TRAPEZOIDAL_SHEET_METAL_ROOF",
              "BEADED_PLATE_ROOF",
              "FACADE",
              "OPEN_FIELD"
            ],
            "description": "The roof type the mounting system is for.",
            "example": "TILED_ROOF"
          }
        },
        "description": "A mounting system (Unterkonstruktion). Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "EmergencyPowerMaterialInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Aiko Neostar 2S 445W"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          }
        },
        "description": "An emergency power product that can be offered with an inverter."
      },
      "EmergencyPowerMaterialUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name as it appears on offers.",
            "example": "Aiko Neostar 2S 445W"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Glas-Glas-Modul, 30 Jahre Leistungsgarantie."
          },
          "manufacturerId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The manufacturer (`GET /v1/manufacturers`).",
            "example": "12"
          },
          "materialSeriesId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The product series the product belongs to, if any.",
            "example": "3"
          },
          "productSeries": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the product series, as free text.",
            "example": "Neostar 2S"
          },
          "manufacturerArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The manufacturer's article number.",
            "example": "A-MAH54Mb-445"
          },
          "internalArticleNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Your own article number. Must be unique across all your active products.",
            "example": "PV-0042"
          },
          "productGuaranteeYears": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Product guarantee in years.",
            "example": 15
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the product qualifies for the 0 % VAT rate for PV systems.",
            "example": true
          },
          "weightG": {
            "type": [
              "number",
              "null"
            ],
            "description": "Weight in grams.",
            "example": 21500
          },
          "dimensions": {
            "$ref": "#/components/schemas/DimensionsInput"
          },
          "datasheetFilePath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded datasheet.",
            "example": "1/datasheets/aiko-neostar-2s.pdf"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Internal notes, not shown to customers.",
            "example": "Lieferzeit ca. 2 Wochen."
          },
          "archived": {
            "type": "boolean",
            "description": "Archived products stay on existing offers but cannot be added to new ones.",
            "example": false
          },
          "favourite": {
            "type": "boolean",
            "description": "Marked as a favourite in the planner.",
            "example": false
          },
          "purchasePrice": {
            "$ref": "#/components/schemas/PurchasePriceInput"
          }
        },
        "description": "An emergency power product that can be offered with an inverter. Send only the fields to change.",
        "example": {
          "purchasePrice": {
            "pricePerPiece": 105.5,
            "vendorName": "Solar-Großhandel Schmidt"
          }
        }
      },
      "Manufacturer": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The manufacturer's id.",
            "example": "12"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name.",
            "example": "Aiko"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text notes.",
            "example": "Module mit ABC-Zellen."
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 30 1234567"
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Website.",
            "example": "https://aikosolar.com"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of the logo, if one was uploaded."
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contact email address.",
            "example": "sales@aikosolar.com"
          },
          "createdAt": {
            "type": "string",
            "description": "When the manufacturer was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "A manufacturer of the products in your catalog."
      },
      "ManufacturerList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Manufacturer"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "ManufacturerInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "Name. Required when creating a manufacturer.",
            "example": "Aiko"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 999,
            "description": "Free-text notes.",
            "example": "Module mit ABC-Zellen."
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 30 1234567"
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 199,
            "description": "Website.",
            "example": "https://aikosolar.com"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded logo.",
            "example": "1/logos/aiko.png"
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contact email address.",
            "example": "sales@aikosolar.com"
          }
        },
        "required": [
          "name"
        ],
        "description": "A manufacturer to create (`POST`) or change (`PATCH`). When changing, send only the fields to change.",
        "example": {
          "name": "Aiko",
          "websiteUrl": "https://aikosolar.com",
          "contactEmail": "sales@aikosolar.com"
        }
      },
      "ManufacturerUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 99,
            "description": "Name. Required when creating a manufacturer.",
            "example": "Aiko"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 999,
            "description": "Free-text notes.",
            "example": "Module mit ABC-Zellen."
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 30 1234567"
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 199,
            "description": "Website.",
            "example": "https://aikosolar.com"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded logo.",
            "example": "1/logos/aiko.png"
          },
          "contactEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Contact email address.",
            "example": "sales@aikosolar.com"
          }
        },
        "description": "Changes to a manufacturer. Send only the fields to change.",
        "example": {
          "contactEmail": "vertrieb@aikosolar.com"
        }
      },
      "Service": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The service's id.",
            "example": "5"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name as it appears on offers.",
            "example": "Gerüst"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Gerüst bis 8 m Traufhöhe, inkl. Auf- und Abbau."
          },
          "includedInOffer": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the service is put on new offers automatically.",
            "example": true
          },
          "fixedPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros.",
            "example": 450
          },
          "pricePerModule": {
            "type": [
              "number",
              "null"
            ],
            "description": "Net price in euros per PV module of the offer, added to `fixedPrice`.",
            "example": 0
          },
          "salesTaxFreeEntitled": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Whether the service qualifies for the 0 % VAT rate for PV systems.",
            "example": false
          },
          "categoryId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The service category, if any.",
            "example": "2"
          },
          "createdAt": {
            "type": "string",
            "description": "When the service was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "A service you offer, such as installation or scaffolding, ready to add to offers."
      },
      "ServiceList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Service"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "ServiceInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Name as it appears on offers.",
            "example": "Gerüst"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Gerüst bis 8 m Traufhöhe, inkl. Auf- und Abbau."
          },
          "includedInOffer": {
            "type": "boolean",
            "description": "Put the service on new offers automatically.",
            "example": true
          },
          "fixedPrice": {
            "type": "number",
            "description": "Net price in euros.",
            "example": 450
          },
          "pricePerModule": {
            "type": "number",
            "description": "Net price in euros per PV module of the offer, added to `fixedPrice`. `0` for none.",
            "example": 0
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the service qualifies for the 0 % VAT rate for PV systems.",
            "example": false
          }
        },
        "required": [
          "name",
          "includedInOffer",
          "fixedPrice",
          "pricePerModule"
        ],
        "description": "A service to create (`POST`) or change (`PATCH`). Creating needs `name`, `includedInOffer`, `fixedPrice` and `pricePerModule`; when changing, send only the fields to change.",
        "example": {
          "name": "Gerüst",
          "description": "Gerüst bis 8 m Traufhöhe, inkl. Auf- und Abbau.",
          "includedInOffer": true,
          "fixedPrice": 450,
          "pricePerModule": 0
        }
      },
      "ServiceUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Name as it appears on offers.",
            "example": "Gerüst"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown on offers.",
            "example": "Gerüst bis 8 m Traufhöhe, inkl. Auf- und Abbau."
          },
          "includedInOffer": {
            "type": "boolean",
            "description": "Put the service on new offers automatically.",
            "example": true
          },
          "fixedPrice": {
            "type": "number",
            "description": "Net price in euros.",
            "example": 450
          },
          "pricePerModule": {
            "type": "number",
            "description": "Net price in euros per PV module of the offer, added to `fixedPrice`. `0` for none.",
            "example": 0
          },
          "salesTaxFreeEntitled": {
            "type": "boolean",
            "description": "Whether the service qualifies for the 0 % VAT rate for PV systems.",
            "example": false
          }
        },
        "description": "Changes to a service. Send only the fields to change.",
        "example": {
          "fixedPrice": 490
        }
      },
      "CompanyMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The team member's id (a uuid). Pass it as `associatedCompanyMemberId` on projects and offers, or as `plannerMemberId` on bookings.",
            "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name.",
            "example": "Max Mustermann"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address they sign in with.",
            "example": "max@solario.example"
          },
          "siteId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The company site the member belongs to (`GET /v1/company/sites`).",
            "example": "1"
          },
          "roleId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The member's role.",
            "example": "4"
          },
          "roleName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the role, as set up in the planner.",
            "example": "Projektplaner"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the member was added (ISO 8601).",
            "example": "2026-03-02T09:00:00+00:00"
          }
        },
        "required": [
          "id"
        ],
        "description": "A member of your team in the planner."
      },
      "CompanyMemberList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanyMember"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "CompanySite": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The site's id. Pass it as `associatedCompanySiteId` on projects.",
            "example": "1"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name.",
            "example": "München"
          },
          "vatPercent": {
            "type": [
              "number",
              "null"
            ],
            "description": "VAT rate in percent for the offers of the site's projects.",
            "example": 19
          },
          "supportEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address customers of this site are told to write to.",
            "example": "muenchen@solario.example"
          },
          "supportPhoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number customers of this site are told to call.",
            "example": "+49 89 7654321"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the site was created (ISO 8601).",
            "example": "2026-01-15T08:00:00+00:00"
          }
        },
        "required": [
          "id"
        ],
        "description": "A site (Standort) of your company. Each site has its own margins and VAT rate, which the prices of its projects' offers follow."
      },
      "CompanySiteList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CompanySite"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "Company": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Your company's id.",
            "example": "1"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Company name.",
            "example": "Solario GmbH"
          },
          "supportPhoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number customers are told to call.",
            "example": "+49 30 1234567"
          },
          "supportEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address customers are told to write to.",
            "example": "support@solario.example"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of the company logo."
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Website.",
            "example": "https://solario.example"
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "description": "Company address."
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "description": "When the company was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "Your company profile: the contact details, address and branding your customers see."
      },
      "CompanyInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Company name.",
            "example": "Solario GmbH"
          },
          "supportPhoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number customers are told to call.",
            "example": "+49 30 1234567"
          },
          "supportEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address customers are told to write to.",
            "example": "support@solario.example"
          },
          "logoPath": {
            "type": [
              "string",
              "null"
            ],
            "description": "Storage path of an uploaded logo.",
            "example": "1/logos/solario.png"
          },
          "websiteUrl": {
            "type": [
              "string",
              "null"
            ],
            "maxLength": 199,
            "description": "Website.",
            "example": "https://solario.example"
          },
          "address": {
            "type": "object",
            "properties": {
              "zip": {
                "type": "string",
                "description": "Postal code.",
                "example": "80331"
              },
              "city": {
                "type": "string",
                "description": "Town or city.",
                "example": "München"
              },
              "street": {
                "type": "string",
                "description": "Street name.",
                "example": "Lindenstraße"
              },
              "streetNumber": {
                "type": "string",
                "description": "House number.",
                "example": "12"
              }
            },
            "description": "Company address. Send only the parts you want to set."
          }
        },
        "description": "Changes to your company profile. Send only the fields to change.",
        "example": {
          "supportEmail": "kundenservice@solario.example",
          "supportPhoneNumber": "+49 30 7654321"
        }
      },
      "AnalyticsSeriesPoint": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "First day of the period: the day itself, its Monday or the first of the month (German calendar).",
            "example": "2026-07-01"
          },
          "count": {
            "type": "integer",
            "description": "How many there were in this period.",
            "example": 42
          }
        },
        "required": [
          "date",
          "count"
        ],
        "description": "One period of a time series."
      },
      "AnalyticsSeries": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsSeriesPoint"
            },
            "description": "One point per period, oldest first. Periods without any are left out."
          }
        },
        "required": [
          "data"
        ]
      },
      "AnalyticsConversionPoint": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "Day the requests came in (German calendar).",
            "example": "2026-07-01"
          },
          "count": {
            "type": "integer",
            "description": "Requests that came in that day.",
            "example": 12
          },
          "converted": {
            "type": "integer",
            "description": "How many of them converted. The rate is `converted / count`.",
            "example": 3
          }
        },
        "required": [
          "date",
          "count",
          "converted"
        ],
        "description": "The requests of one day and how many of them converted."
      },
      "AnalyticsConversions": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsConversionPoint"
            },
            "description": "One point per day with requests, oldest first."
          }
        },
        "required": [
          "data"
        ]
      },
      "AnalyticsDemographics": {
        "type": "object",
        "properties": {
          "customerCount": {
            "type": "integer",
            "description": "All your customers.",
            "example": 412
          },
          "maleCount": {
            "type": "integer",
            "description": "Customers with the salutation Herr (`gender: MALE`).",
            "example": 190
          },
          "femaleCount": {
            "type": "integer",
            "description": "Customers with the salutation Frau (`gender: FEMALE`). The rest have none.",
            "example": 171
          }
        },
        "required": [
          "customerCount",
          "maleCount",
          "femaleCount"
        ],
        "description": "How many customers you have, by salutation."
      },
      "AnalyticsProductSale": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "description": "Day of the sale (German calendar).",
            "example": "2026-07-14"
          },
          "productKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "`category:materialId`. Unique per product, unlike `materialId` alone.",
            "example": "BATTERY:88"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "The product type: `PV_MODULE`, `INVERTER`, `BATTERY`, `WALLBOX`, `SUBCONSTRUCTION`, `EQUIPMENT`, `MISC` or `EMERGENCY_POWER`.",
            "example": "BATTERY"
          },
          "materialId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The material's id (unique per type only).",
            "example": "88"
          },
          "productName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product name.",
            "example": "BYD Battery-Box Premium HVS 10.2"
          },
          "manufacturerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Manufacturer name.",
            "example": "BYD"
          },
          "soldUnits": {
            "type": [
              "number",
              "null"
            ],
            "description": "Units sold that day.",
            "example": 2
          },
          "soldOffersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Accepted offers with the product that day.",
            "example": 2
          },
          "lastSoldAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the last of them was accepted.",
            "example": "2026-07-14T15:02:11+00:00"
          }
        },
        "required": [
          "date"
        ],
        "description": "One product's sales on one day."
      },
      "AnalyticsProductSaleList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsProductSale"
            },
            "description": "The rows on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many rows match, across all pages.",
            "example": 37
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many rows were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "AnalyticsOpenOffer": {
        "type": "object",
        "properties": {
          "productKey": {
            "type": [
              "string",
              "null"
            ],
            "description": "`category:materialId`. Unique per product, unlike `materialId` alone.",
            "example": "BATTERY:88"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "The product type: `PV_MODULE`, `INVERTER`, `BATTERY`, `WALLBOX`, `SUBCONSTRUCTION`, `EQUIPMENT`, `MISC` or `EMERGENCY_POWER`.",
            "example": "BATTERY"
          },
          "materialId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The material's id (unique per type only).",
            "example": "88"
          },
          "productName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Product name.",
            "example": "BYD Battery-Box Premium HVS 10.2"
          },
          "manufacturerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Manufacturer name.",
            "example": "BYD"
          },
          "openUnits": {
            "type": [
              "number",
              "null"
            ],
            "description": "Units in offers that are still open.",
            "example": 96
          },
          "openOffersCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Open offers with the product.",
            "example": 4
          }
        },
        "description": "One product in your open offers."
      },
      "AnalyticsOpenOfferList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalyticsOpenOffer"
            },
            "description": "The rows on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many rows match, across all pages.",
            "example": 23
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many rows were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "GridOperatorDocument": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this in `documentIds` when generating documents.",
            "example": "4"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The form's name, used for the file name in the archive.",
            "example": "Anmeldung Erzeugungsanlage"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text note from your document configuration."
          },
          "pass": {
            "type": [
              "string",
              "null"
            ],
            "description": "How often the form is filled in: `OFFER` once per offer, `INVERTER` once per inverter, `BATTERY` once per battery.",
            "example": "OFFER"
          },
          "orderPriority": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Position in the operator's order; documents always come out in this order.",
            "example": 1
          }
        },
        "required": [
          "id"
        ],
        "description": "One registration form configured for a grid operator."
      },
      "GridOperator": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this as `networkCarrierId` when generating documents.",
            "example": "7"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The operator's name.",
            "example": "Stadtwerke München"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text notes."
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GridOperatorDocument"
            },
            "description": "The operator's forms, in the configured order."
          },
          "createdAt": {
            "type": "string",
            "description": "When the operator was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "documents",
          "createdAt"
        ],
        "description": "The operator set on the offer's project, with its forms — the default for `networkCarrierId` and `documentIds`. `null` when the project has none, in which case `networkCarrierId` is required."
      },
      "ElectricalFirm": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this as `electronicsFirmId` when generating documents.",
            "example": "3"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "Company name.",
            "example": "Elektro Huber GmbH"
          },
          "firmNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "The firm's registration number with the grid operator.",
            "example": "EL-2019-4471"
          },
          "responsibleElectricianName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The electrician named on the forms.",
            "example": "Josef Huber"
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Email address.",
            "example": "info@elektro-huber.example"
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 89 7654321"
          },
          "address": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "description": "Postal address."
              }
            ]
          },
          "createdAt": {
            "type": "string",
            "description": "When the contractor was created (ISO 8601).",
            "example": "2026-07-01T10:22:00+00:00"
          }
        },
        "required": [
          "id",
          "createdAt"
        ],
        "description": "An electrical contractor (Elektrofachbetrieb) that can be named on grid-operator forms."
      },
      "ModuleConfigurationOption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this in `moduleConfigurationIds`.",
            "example": "19"
          },
          "roofName": {
            "type": [
              "string",
              "null"
            ],
            "example": "Süddach"
          },
          "moduleName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The module's product name — `null` unless the key also carries `read:materials`. The id works regardless.",
            "example": "Vitovolt 300-DG M440HC"
          },
          "modulesCount": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Modules on the roof this configuration belongs to.",
            "example": 24
          },
          "selected": {
            "type": "boolean",
            "description": "`true` for the configuration currently active on its roof — what the planner UI preselects."
          },
          "roofActive": {
            "type": "boolean",
            "description": "`false` when the roof itself is not part of the offer."
          }
        },
        "required": [
          "id",
          "selected",
          "roofActive"
        ],
        "description": "A module configuration of the offer that the technical data can be restricted to."
      },
      "BatteryOption": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Pass this as `batteryId`.",
            "example": "88"
          },
          "name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The product name — `null` unless the key also carries `read:materials`, which is what the catalog is gated on. The id works regardless.",
            "example": "Vitocharge VX3 8 kWh"
          },
          "selected": {
            "type": "boolean",
            "description": "`true` for the battery marked active on the offer."
          }
        },
        "required": [
          "id",
          "selected"
        ],
        "description": "A battery configured on the offer."
      },
      "NetworkCarrierDocumentOptions": {
        "type": "object",
        "properties": {
          "gridOperator": {
            "$ref": "#/components/schemas/GridOperator"
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GridOperatorDocument"
            },
            "description": "Convenience copy of `gridOperator.documents`: the forms that a POST with no `documentIds` will produce, in order."
          },
          "electricalFirms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ElectricalFirm"
            },
            "description": "Contractors you can pass as `electronicsFirmId`."
          },
          "moduleConfigurations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleConfigurationOption"
            }
          },
          "batteries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatteryOption"
            }
          }
        },
        "required": [
          "gridOperator",
          "documents",
          "electricalFirms",
          "moduleConfigurations",
          "batteries"
        ],
        "description": "Everything the grid-operator document request for this offer can reference, resolved in one call."
      },
      "NetworkCarrierDocumentsInput": {
        "type": "object",
        "properties": {
          "networkCarrierId": {
            "type": "string",
            "pattern": "^\\d+$",
            "description": "The grid operator whose forms to fill; its document set defines which `documentIds` are valid. Omit to use the operator set on the offer's project. List the operators with `GET /v1/grid-operators`.",
            "example": "7"
          },
          "documentIds": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^\\d+$"
            },
            "minItems": 1,
            "description": "Which of the operator's documents to produce. Omit to produce all of them. They are always generated in the order the operator configured, whatever order you send.",
            "example": [
              "1",
              "4"
            ]
          },
          "plannedGoingLiveDate": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Planned commissioning date, written into the forms that ask for one.",
            "example": "2026-09-01"
          },
          "electronicsFirmId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "The electrical contractor to name on the forms, from `GET /v1/electrical-firms`. Defaults to the one on the project.",
            "example": "3"
          },
          "moduleConfigurationIds": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^\\d+$"
            },
            "description": "Restrict the technical data to these module configurations of the offer. Omit to include all of them.",
            "example": [
              "19"
            ]
          },
          "batteryId": {
            "type": [
              "string",
              "null"
            ],
            "pattern": "^\\d+$",
            "description": "Which battery on the offer to describe. Omit and the offer's selected battery is used when there is exactly one candidate; send `null` to describe no battery at all. `BATTERY`-pass documents come out empty without one.",
            "example": "88"
          },
          "includeCertificates": {
            "type": "boolean",
            "description": "Add the products' certificates to the archive.",
            "example": true
          },
          "includeDatasheets": {
            "type": "boolean",
            "description": "Add the products' datasheets to the archive.",
            "example": true
          },
          "includeMergedDocument": {
            "type": "boolean",
            "description": "Also add one PDF with all generated documents merged, next to the single files.",
            "example": true
          }
        },
        "description": "Which grid-operator documents to generate for an offer, and what to put on them. Every field is optional — an empty body produces the project operator's full document set."
      },
      "Booking": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The booking's id (a uuid).",
            "example": "a0000000-0000-4000-8000-000000000001"
          },
          "source": {
            "type": "string",
            "description": "Where the booking came from: `BUILDER` (your configurator; the only source that also creates an offer request), `BOOKING_PAGE` (your shared booking link) or `PLANNER` (entered by your team or through this API).",
            "example": "BUILDER"
          },
          "status": {
            "type": "string",
            "description": "`PENDING_PAYMENT` (in the customer's checkout), `CONFIRMED`, `CANCELLED`, `COMPLETED`, `NO_SHOW` or `EXPIRED` (checkout not finished in time).",
            "example": "CONFIRMED"
          },
          "paymentStatus": {
            "type": "string",
            "description": "`FREE`, `UNPAID`, `PAID` or `REFUNDED`.",
            "example": "PAID"
          },
          "typeSlug": {
            "type": [
              "string",
              "null"
            ],
            "description": "The appointment type.",
            "example": "erstgespraech"
          },
          "typeName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Name of the appointment type.",
            "example": "Erstgespräch"
          },
          "durationMinutes": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Length in minutes.",
            "example": 30
          },
          "locationKind": {
            "type": [
              "string",
              "null"
            ],
            "description": "`VIDEO`, `PHONE` or `ON_SITE`.",
            "example": "VIDEO"
          },
          "startsAt": {
            "type": "string",
            "description": "Start (ISO 8601).",
            "example": "2026-09-15T10:00:00+00:00"
          },
          "endsAt": {
            "type": "string",
            "description": "End (ISO 8601).",
            "example": "2026-09-15T10:30:00+00:00"
          },
          "priceCents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Price in cents, including VAT.",
            "example": 7900
          },
          "currency": {
            "type": [
              "string",
              "null"
            ],
            "description": "Currency of the price.",
            "example": "EUR"
          },
          "customer": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The customer's name, always filled; use it for display.",
                "example": "Anna Müller"
              },
              "email": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Email address.",
                "example": "anna.mueller@example.com"
              },
              "phone": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Phone number. Needs `read:customers`.",
                "example": "+49 89 1234567"
              },
              "forename": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "First name as typed on the booking form. `null` for a booking your team entered, which has a single name field; it is never guessed by splitting `name`.",
                "example": "Anna"
              },
              "surname": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Last name as typed on the booking form; `null` like `forename`.",
                "example": "Müller"
              },
              "address": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Address as the customer typed it, in one line. Needs `read:customers`.",
                "example": "Lindenstraße 12, 80331 München"
              },
              "billingName": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Who the invoice goes to, as confirmed in the Stripe checkout; can differ from `name`, e.g. when a company pays. Needs `read:customers`.",
                "example": "Müller Haustechnik GmbH"
              },
              "billingAddress": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "line1": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Street and house number.",
                    "example": "Musterstraße 12"
                  },
                  "line2": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Address addition."
                  },
                  "postalCode": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Postal code.",
                    "example": "04109"
                  },
                  "city": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Town or city.",
                    "example": "Leipzig"
                  },
                  "state": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Region; usually `null` for German addresses."
                  },
                  "country": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Country code (ISO 3166-1 alpha-2).",
                    "example": "DE"
                  }
                },
                "description": "The billing address the customer confirmed when paying through Stripe. `null` for free and manually settled bookings."
              }
            },
            "description": "The customer. Phone, address, billing details and `notes` are only filled in for an API key that also has `read:customers`; otherwise they are `null`."
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the customer wrote when booking. Needs `read:customers`.",
            "example": "Bitte vorher anrufen."
          },
          "plannerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "The team member the appointment is assigned to. Only filled in for an API key that also has `read:company`.",
            "example": "Max Mustermann"
          },
          "offerRequestId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The offer request a `BUILDER` booking created; read it with `GET /v1/offer-requests/{id}`. A follow-up carries the offer request of the booking it follows.",
            "example": "501"
          },
          "followUpOf": {
            "type": [
              "string",
              "null"
            ],
            "description": "The booking this one follows up (see `POST /v1/bookings/{id}/follow-up`); `null` for every other booking.",
            "example": "a0000000-0000-4000-8000-000000000003"
          },
          "paidAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the payment went through.",
            "example": "2026-09-01T08:16:00+00:00"
          },
          "amountPaidCents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "What was actually paid, in cents. Less than `priceCents` when the customer used a promotion code; invoice this amount. `null` for free and manually settled bookings.",
            "example": 7110
          },
          "paymentMethod": {
            "type": [
              "string",
              "null"
            ],
            "description": "`card`, `paypal`, `sepa_debit`, or `manual` for a booking settled outside Stripe.",
            "example": "card"
          },
          "taxRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "The VAT percentage contained in `priceCents` (prices include VAT). `0` means the appointment type has no VAT.",
            "example": 19
          },
          "invoice": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "number": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Invoice number.",
                "example": "ABCD-0001"
              },
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Invoice page at Stripe."
              },
              "pdfUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Direct link to the PDF."
              }
            },
            "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; new bookings are invoiced in your accounting, so this is `null`."
          },
          "stripe": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "customerId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Stripe customer.",
                "example": "cus_QwErTy123456"
              },
              "paymentIntentId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Stripe payment.",
                "example": "pi_3PabcdEFGH123456"
              },
              "invoiceId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Stripe invoice. Only bookings paid before Stripe invoices were switched off have one; `null` for new bookings, which are invoiced in your accounting.",
                "example": "in_1PabcdEFGH123456"
              }
            },
            "description": "References into your own Stripe account, for reconciliation. `null` for a booking that never went through Stripe."
          },
          "refundedAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the money went back."
          },
          "refundAmountCents": {
            "type": [
              "integer",
              "null"
            ],
            "description": "How much went back, in cents.",
            "example": 7900
          },
          "cancelledAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the booking was cancelled."
          },
          "cancellationReason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reason given for the cancellation.",
            "example": "Termin passt nicht mehr."
          },
          "createdAt": {
            "type": "string",
            "description": "When the booking was made (ISO 8601).",
            "example": "2026-09-01T08:15:00+00:00"
          }
        },
        "required": [
          "id",
          "source",
          "status",
          "paymentStatus",
          "startsAt",
          "endsAt",
          "createdAt"
        ],
        "description": "A booked appointment."
      },
      "BookingList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Booking"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "AppointmentType": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "The type's fixed identifier. Use it as `typeSlug` when booking.",
            "example": "erstgespraech"
          },
          "name": {
            "type": "string",
            "description": "Name shown to the customer.",
            "example": "Erstgespräch"
          },
          "description": {
            "type": [
              "string",
              "null"
            ],
            "description": "Description shown to the customer.",
            "example": "Wir besprechen Ihr Dach und Ihren Strombedarf."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Length of the appointment in minutes.",
            "example": 30
          },
          "priceCents": {
            "type": "integer",
            "description": "Price in cents, including VAT. `0` means free: the booking is confirmed immediately.",
            "example": 0
          },
          "currency": {
            "type": "string",
            "description": "Currency of the price.",
            "example": "EUR"
          },
          "locationKind": {
            "type": "string",
            "description": "Where it takes place: `VIDEO`, `PHONE` or `ON_SITE`.",
            "example": "VIDEO"
          },
          "active": {
            "type": "boolean",
            "description": "Inactive types cannot be booked.",
            "example": true
          }
        },
        "required": [
          "slug",
          "name",
          "durationMinutes",
          "priceCents",
          "currency",
          "locationKind",
          "active"
        ],
        "description": "A kind of appointment your customers can book."
      },
      "AppointmentTypeList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AppointmentType"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "BookingInput": {
        "type": "object",
        "properties": {
          "typeSlug": {
            "type": "string",
            "minLength": 1,
            "description": "The appointment type to book, from `GET /v1/appointment-types`.",
            "example": "erstgespraech"
          },
          "startsAt": {
            "type": "string",
            "minLength": 1,
            "description": "Start of the appointment (ISO 8601). It must be a free slot of that type; otherwise the call answers `409 slot_unavailable`.",
            "example": "2026-09-15T10:00:00+02:00"
          },
          "customerName": {
            "type": "string",
            "minLength": 1,
            "description": "The customer's name.",
            "example": "Anna Müller"
          },
          "customerEmail": {
            "type": "string",
            "minLength": 1,
            "description": "The customer's email address; the confirmation goes there.",
            "example": "anna.mueller@example.com"
          },
          "customerPhone": {
            "type": [
              "string",
              "null"
            ],
            "description": "Phone number.",
            "example": "+49 89 1234567"
          },
          "customerAddress": {
            "type": [
              "string",
              "null"
            ],
            "description": "Address in one line, e.g. for an appointment on site.",
            "example": "Lindenstraße 12, 80331 München"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Notes for your team.",
            "example": "Bitte vorher anrufen."
          },
          "plannerMemberId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The team member to give the appointment to (`GET /v1/company/members`); they must be free at that time. Leave it out and the free team member with the fewest appointments gets it.",
            "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
          }
        },
        "required": [
          "typeSlug",
          "startsAt",
          "customerName",
          "customerEmail"
        ],
        "description": "An appointment to book for a customer.",
        "example": {
          "typeSlug": "erstgespraech",
          "startsAt": "2026-09-15T10:00:00+02:00",
          "customerName": "Anna Müller",
          "customerEmail": "anna.mueller@example.com",
          "customerPhone": "+49 89 1234567"
        }
      },
      "BookingFollowUpInput": {
        "type": "object",
        "properties": {
          "startsAt": {
            "type": "string",
            "minLength": 1,
            "description": "Start of the follow-up (ISO 8601). It must be a free slot; otherwise the call answers `409 slot_unavailable`.",
            "example": "2026-09-29T10:00:00+02:00"
          },
          "typeSlug": {
            "type": "string",
            "minLength": 1,
            "description": "Appointment type of the follow-up, from `GET /v1/appointment-types`. Leave it out to book the same type again.",
            "example": "detailplanung"
          },
          "plannerMemberId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Who takes the follow-up (`GET /v1/company/members`). Leave it out to keep the team member of the earlier booking (they must be free then), send an id to choose someone else, or `null` to give it to the free team member with the fewest appointments.",
            "example": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ],
            "description": "Notes for your team.",
            "example": "Angebot besprechen."
          }
        },
        "required": [
          "startsAt"
        ],
        "description": "A follow-up appointment for the customer of an existing booking. Who the customer is comes from that booking.",
        "example": {
          "startsAt": "2026-09-29T10:00:00+02:00",
          "typeSlug": "detailplanung"
        }
      },
      "BookingCancelInput": {
        "type": "object",
        "properties": {
          "refund": {
            "type": "boolean",
            "description": "Give a payment through Stripe back. Ignored for unpaid and free bookings.",
            "example": true
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Shown to the customer in the cancellation email.",
            "example": "Termin passt nicht mehr."
          }
        },
        "description": "How to cancel the booking.",
        "example": {
          "refund": true,
          "reason": "Termin passt nicht mehr."
        }
      },
      "BookingRescheduleInput": {
        "type": "object",
        "properties": {
          "startsAt": {
            "type": "string",
            "minLength": 1,
            "description": "The new start (ISO 8601). It must be a free slot; otherwise the call answers `409 slot_unavailable`.",
            "example": "2026-09-17T14:00:00+02:00"
          }
        },
        "required": [
          "startsAt"
        ],
        "description": "When the appointment moves to."
      },
      "GridOperatorList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "allOf": [
                {
                  "$ref": "#/components/schemas/GridOperator"
                },
                {
                  "type": "object",
                  "description": "A grid operator (Netzbetreiber) you register installations with, together with the forms configured for it."
                }
              ]
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      },
      "ElectricalFirmList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ElectricalFirm"
            },
            "description": "The records on this page."
          },
          "total": {
            "type": "integer",
            "description": "How many records match the filters in total, across all pages.",
            "example": 128
          },
          "limit": {
            "type": "integer",
            "description": "The page size used.",
            "example": 50
          },
          "offset": {
            "type": "integer",
            "description": "How many records were skipped.",
            "example": 0
          }
        },
        "required": [
          "data",
          "total",
          "limit",
          "offset"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/v1/projects": {
      "get": {
        "summary": "List projects",
        "description": "Returns your projects, newest first. Filter with `name`, `state`, `customerId` and `city`, sort with `sort` (e.g. `sort=name.asc`) and page with `limit` and `offset`. Requires the `read:projects` scope.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name, state, offersCount, activeOffersCount."
          },
          {
            "schema": {
              "type": "string",
              "example": "Müller"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only projects whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "DETAIL_PHASE"
            },
            "required": false,
            "name": "state",
            "in": "query",
            "description": "Only projects in this state: `INDICATION_PHASE`, `DETAIL_PHASE`, `IN_IMPLEMENTATION` or `FINISHED`."
          },
          {
            "schema": {
              "type": "string",
              "example": "77"
            },
            "required": false,
            "name": "customerId",
            "in": "query",
            "description": "Only projects of this customer."
          },
          {
            "schema": {
              "type": "string",
              "example": "München"
            },
            "required": false,
            "name": "city",
            "in": "query",
            "description": "Only projects whose site is in a city containing this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of projects.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a project",
        "description": "Creates a project for a customer, together with its roofs. You need the customer's `id` (from `POST /v1/customers`) and the team member responsible for the project. Requires the `write:projects` scope.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectInput"
              },
              "examples": {
                "withRoof": {
                  "summary": "Project with one roof",
                  "value": {
                    "name": "PV Müller Satteldach",
                    "customerId": "77",
                    "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
                    "associatedCompanySiteId": "1",
                    "electricityPriceCentPerKwh": 35,
                    "customerElectricityConsumptionKwhPerYear": 4500,
                    "address": {
                      "street": "Lindenstraße",
                      "streetNumber": "12",
                      "zip": "80331",
                      "city": "München"
                    },
                    "roofs": [
                      {
                        "name": "Süddach",
                        "azimuth": 180,
                        "tilt": 35,
                        "pvCloudingType": "NO_CLOUDING",
                        "roofType": "TILED_ROOF"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{id}": {
      "get": {
        "summary": "Get a project",
        "description": "Returns one project. Requires the `read:projects` scope.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "41"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The project's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a project",
        "description": "Changes a project. Send only the fields to change; the others keep their value. If you send `roofs`, send all of them: roofs you leave out are deleted, unless an offer uses them (`409 in_use`). Requires the `write:projects` scope.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "41"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The project's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdate"
              },
              "examples": {
                "consumption": {
                  "summary": "New consumption and electricity price",
                  "value": {
                    "customerElectricityConsumptionKwhPerYear": 6000,
                    "electricityPriceCentPerKwh": 38
                  }
                },
                "roofs": {
                  "summary": "Change one roof and add another",
                  "description": "Changes roof 301, adds a flat garage roof and deletes the project's other roofs.",
                  "value": {
                    "roofs": [
                      {
                        "name": "Süddach",
                        "azimuth": 180,
                        "tilt": 30,
                        "pvCloudingType": "NO_CLOUDING",
                        "roofType": "TILED_ROOF",
                        "id": "301"
                      },
                      {
                        "name": "Garage",
                        "azimuth": 90,
                        "tilt": 10,
                        "pvCloudingType": "LITTLE_CLOUDING",
                        "roofType": "BITUMEN_ROOF"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The project after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: a roof that `roofs` leaves out is used by an offer, or is part of a published offer. Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a project",
        "description": "Deletes a project together with all of its offers, also published and accepted ones; the customer can no longer see them. This cannot be undone. Requires the `write:projects` scope.",
        "tags": [
          "Projects"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "41"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The project's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The project and its offers were deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers": {
      "get": {
        "summary": "List offers",
        "description": "Returns your offers, newest first. Filter with `name`, `state`, `projectId`, `customerId` and `isIndication`, sort with `sort` (e.g. `sort=offerPrice.desc`) and page with `limit` and `offset`. Requires the `read:offers` scope.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name, state, validTo, offerPrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "9,8 kWp"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only offers whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "ACCEPTED"
            },
            "required": false,
            "name": "state",
            "in": "query",
            "description": "Only offers in this state: `DRAFT`, `PUBLISHED`, `ACCEPTED` or `DECLINED`."
          },
          {
            "schema": {
              "type": "string",
              "example": "41"
            },
            "required": false,
            "name": "projectId",
            "in": "query",
            "description": "Only offers of this project."
          },
          {
            "schema": {
              "type": "string",
              "example": "77"
            },
            "required": false,
            "name": "customerId",
            "in": "query",
            "description": "Only offers of this customer."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "isIndication",
            "in": "query",
            "description": "`true` for indication offers only, `false` for detailed offers only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of offers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an offer",
        "description": "Creates an offer as a draft, which the customer cannot see yet. Send it in one go, or start with the `offer` part alone and add lines later with the `/v1/offers/{offerId}/…` endpoints. Publish it with `POST /v1/offers/{id}/state`. Requires the `write:offers` scope.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferInput"
              },
              "examples": {
                "complete": {
                  "summary": "Offer with roof, modules, inverter, battery, service and discount",
                  "value": {
                    "offer": {
                      "projectId": "41",
                      "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
                      "name": "Angebot PV 9,8 kWp mit Speicher",
                      "validTo": "2026-10-31",
                      "applySalesTaxFreeEntitled": true
                    },
                    "roofs": [
                      {
                        "projectRoofConfigurationId": "301",
                        "active": true,
                        "pvModulesCount": 22,
                        "moduleConfigs": [
                          {
                            "materialPvModuleId": "150",
                            "recommended": true,
                            "netPricePerModule": 98
                          }
                        ],
                        "subconstruction": {
                          "materialSubconstructionId": "9",
                          "netPrice": 150,
                          "pricePerModule": 32
                        }
                      }
                    ],
                    "inverters": [
                      {
                        "materialInverterId": "70",
                        "netPrice": 1850,
                        "mpptStrings": [
                          {
                            "trackerMaterialInverterMppTrackerId": "141",
                            "roofProjectRoofConfigurationId": "301",
                            "pvModulesCount": 11,
                            "parallelStringsCount": 1
                          },
                          {
                            "trackerMaterialInverterMppTrackerId": "142",
                            "roofProjectRoofConfigurationId": "301",
                            "pvModulesCount": 11,
                            "parallelStringsCount": 1
                          }
                        ]
                      }
                    ],
                    "batteries": [
                      {
                        "materialBatteryId": "88",
                        "active": true,
                        "netPrice": 4200
                      }
                    ],
                    "services": [
                      {
                        "serviceId": "5",
                        "fixedPrice": 450,
                        "category": "ROOF"
                      }
                    ],
                    "additionalCosts": [
                      {
                        "name": "Rabatt Sommeraktion",
                        "fixedPrice": -300
                      }
                    ]
                  }
                },
                "minimal": {
                  "summary": "Empty draft, lines added later",
                  "value": {
                    "offer": {
                      "projectId": "41",
                      "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
                      "name": "Angebot PV 9,8 kWp"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{id}": {
      "get": {
        "summary": "Get an offer",
        "description": "Returns one offer with its state and total price. The lines are listed by the `/v1/offers/{offerId}/…` endpoints. Requires the `read:offers` scope.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offer"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Replace an offer",
        "description": "Replaces a draft completely with the offer you send: lines you leave out are removed. Only drafts can be replaced; once an offer was published, the customer has seen it and the call answers `409 offer_not_draft` — create a new offer instead. Requires the `write:offers` scope.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferInput"
              },
              "examples": {
                "complete": {
                  "summary": "The complete offer",
                  "value": {
                    "offer": {
                      "projectId": "41",
                      "associatedCompanyMemberId": "8f14e45f-ceea-467a-9575-6f1b2c3d4e5f",
                      "name": "Angebot PV 9,8 kWp mit Speicher",
                      "validTo": "2026-10-31",
                      "applySalesTaxFreeEntitled": true
                    },
                    "roofs": [
                      {
                        "projectRoofConfigurationId": "301",
                        "active": true,
                        "pvModulesCount": 22,
                        "moduleConfigs": [
                          {
                            "materialPvModuleId": "150",
                            "recommended": true,
                            "netPricePerModule": 98
                          }
                        ],
                        "subconstruction": {
                          "materialSubconstructionId": "9",
                          "netPrice": 150,
                          "pricePerModule": 32
                        }
                      }
                    ],
                    "inverters": [
                      {
                        "materialInverterId": "70",
                        "netPrice": 1850,
                        "mpptStrings": [
                          {
                            "trackerMaterialInverterMppTrackerId": "141",
                            "roofProjectRoofConfigurationId": "301",
                            "pvModulesCount": 11,
                            "parallelStringsCount": 1
                          },
                          {
                            "trackerMaterialInverterMppTrackerId": "142",
                            "roofProjectRoofConfigurationId": "301",
                            "pvModulesCount": 11,
                            "parallelStringsCount": 1
                          }
                        ]
                      }
                    ],
                    "batteries": [
                      {
                        "materialBatteryId": "88",
                        "active": true,
                        "netPrice": 4200
                      }
                    ],
                    "services": [
                      {
                        "serviceId": "5",
                        "fixedPrice": 450,
                        "category": "ROOF"
                      }
                    ],
                    "additionalCosts": [
                      {
                        "name": "Rabatt Sommeraktion",
                        "fixedPrice": -300
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The offer after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already and cannot be changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an offer",
        "description": "Deletes an offer in any state; a published or accepted offer also disappears for the customer. This cannot be undone. Requires the `write:offers` scope.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The offer was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{id}/state": {
      "post": {
        "summary": "Publish, accept or decline an offer",
        "description": "Moves an offer to another state. An offer goes from `DRAFT` to `PUBLISHED`, and from there to `ACCEPTED` or `DECLINED`. Requires the `write:offers` scope.\n\n- **Publish** (`PUBLISHED`): the customer can see the offer. A draft whose last valid day (`validTo`) is over, or whose price is zero or less, cannot be published (`409 offer_not_publishable`; `message` says which).\n- **Accept** (`ACCEPTED`): records the order, for example one the customer gave you by phone. The offer is taken as offered: on each roof the module variant marked `recommended`, the `active` battery and wallbox, the recommended emergency power, and no optional extras. Possible until the end of the `validTo` day, German time (`409 offer_expired` after that).\n- **Decline** (`DECLINED`): the customer can no longer order it.\n\nAn offer cannot go back to `DRAFT` (`409 offer_not_draft`), and a draft cannot be accepted or declined before it is published (`409 offer_not_published`). To change a published offer, create a new one.",
        "tags": [
          "Offers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferStateInput"
              },
              "examples": {
                "publish": {
                  "summary": "Publish the offer",
                  "value": {
                    "state": "PUBLISHED"
                  }
                },
                "accept": {
                  "summary": "Record the customer's order",
                  "value": {
                    "state": "ACCEPTED"
                  }
                },
                "decline": {
                  "summary": "Mark the offer as declined",
                  "value": {
                    "state": "DECLINED"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The offer in its new state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Offer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The state change is not possible. `offer_not_publishable`: `validTo` is over or the price is zero or less. `offer_not_published`: a draft cannot be accepted or declined. `offer_not_draft`: an offer cannot go back to `DRAFT`. `offer_expired`: the last valid day is over. `offer_selection_required`: a roof has several module variants and none is marked `recommended`, so it is not clear what was ordered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/services": {
      "get": {
        "summary": "List the services of an offer",
        "tags": [
          "Offer services"
        ],
        "description": "Returns all services of an offer. Requires the `read:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer's services; an empty list for an unknown offer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfferService"
                      },
                      "description": "The offer's services."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a service to an offer",
        "tags": [
          "Offer services"
        ],
        "description": "Adds a service to a draft offer. The offer's total price is updated. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferServiceInput"
              },
              "examples": {
                "example": {
                  "summary": "Add a service",
                  "value": {
                    "serviceId": "5",
                    "fixedPrice": 450,
                    "category": "ROOF"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferService"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `already_exists`: this service is on the offer already (batteries and wallboxes only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/services/{itemId}": {
      "put": {
        "summary": "Change a service on an offer",
        "tags": [
          "Offer services"
        ],
        "description": "Replaces a service of a draft offer with what you send; fields you leave out go back to their default. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "1204"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/services`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferServiceInput"
              },
              "examples": {
                "example": {
                  "summary": "The service after the change",
                  "value": {
                    "serviceId": "5",
                    "fixedPrice": 450,
                    "category": "ROOF"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferService"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a service from an offer",
        "tags": [
          "Offer services"
        ],
        "description": "Removes a service from a draft offer. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "1204"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/services`."
          }
        ],
        "responses": {
          "204": {
            "description": "The service was removed."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `in_use`: the line is part of what the customer was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/additional-costs": {
      "get": {
        "summary": "List the cost lines of an offer",
        "tags": [
          "Offer additional costs"
        ],
        "description": "Returns all cost lines of an offer. Requires the `read:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer's cost lines; an empty list for an unknown offer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfferAdditionalCost"
                      },
                      "description": "The offer's cost lines."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a cost line to an offer",
        "tags": [
          "Offer additional costs"
        ],
        "description": "Adds a cost line to a draft offer. The offer's total price is updated. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferAdditionalCostInput"
              },
              "examples": {
                "example": {
                  "summary": "Add a cost line",
                  "value": {
                    "name": "Rabatt Sommeraktion",
                    "fixedPrice": -300,
                    "description": "Gültig bis 31.08."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new cost line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferAdditionalCost"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `already_exists`: this cost line is on the offer already (batteries and wallboxes only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/additional-costs/{itemId}": {
      "put": {
        "summary": "Change a cost line on an offer",
        "tags": [
          "Offer additional costs"
        ],
        "description": "Replaces a cost line of a draft offer with what you send; fields you leave out go back to their default. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "530"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/additional-costs`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferAdditionalCostInput"
              },
              "examples": {
                "example": {
                  "summary": "The cost line after the change",
                  "value": {
                    "name": "Rabatt Sommeraktion",
                    "fixedPrice": -300,
                    "description": "Gültig bis 31.08."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cost line after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferAdditionalCost"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a cost line from an offer",
        "tags": [
          "Offer additional costs"
        ],
        "description": "Removes a cost line from a draft offer. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "530"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/additional-costs`."
          }
        ],
        "responses": {
          "204": {
            "description": "The cost line was removed."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `in_use`: the line is part of what the customer was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/batteries": {
      "get": {
        "summary": "List the batteries of an offer",
        "tags": [
          "Offer batteries"
        ],
        "description": "Returns all batteries of an offer. Requires the `read:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer's batteries; an empty list for an unknown offer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfferBattery"
                      },
                      "description": "The offer's batteries."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a battery to an offer",
        "tags": [
          "Offer batteries"
        ],
        "description": "Adds a battery to a draft offer. The offer's total price is updated. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferBatteryInput"
              },
              "examples": {
                "example": {
                  "summary": "Add a battery",
                  "value": {
                    "materialBatteryId": "88",
                    "netPrice": 4200,
                    "active": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new battery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferBattery"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `already_exists`: this battery is on the offer already (batteries and wallboxes only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/batteries/{itemId}": {
      "put": {
        "summary": "Change a battery on an offer",
        "tags": [
          "Offer batteries"
        ],
        "description": "Replaces a battery of a draft offer with what you send; fields you leave out go back to their default. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "88"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The battery's material id (`materialBatteryId`); a battery is on an offer at most once."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferBatteryInput"
              },
              "examples": {
                "example": {
                  "summary": "The battery after the change",
                  "value": {
                    "materialBatteryId": "88",
                    "netPrice": 4200,
                    "active": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The battery after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferBattery"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a battery from an offer",
        "tags": [
          "Offer batteries"
        ],
        "description": "Removes a battery from a draft offer. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "88"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The battery's material id (`materialBatteryId`); a battery is on an offer at most once."
          }
        ],
        "responses": {
          "204": {
            "description": "The battery was removed."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `in_use`: the line is part of what the customer was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/wallboxes": {
      "get": {
        "summary": "List the wallboxes of an offer",
        "tags": [
          "Offer wallboxes"
        ],
        "description": "Returns all wallboxes of an offer. Requires the `read:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer's wallboxes; an empty list for an unknown offer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfferWallbox"
                      },
                      "description": "The offer's wallboxes."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a wallbox to an offer",
        "tags": [
          "Offer wallboxes"
        ],
        "description": "Adds a wallbox to a draft offer. The offer's total price is updated. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferWallboxInput"
              },
              "examples": {
                "example": {
                  "summary": "Add a wallbox",
                  "value": {
                    "materialWallboxId": "25",
                    "netPrice": 790,
                    "active": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new wallbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferWallbox"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `already_exists`: this wallbox is on the offer already (batteries and wallboxes only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/wallboxes/{itemId}": {
      "put": {
        "summary": "Change a wallbox on an offer",
        "tags": [
          "Offer wallboxes"
        ],
        "description": "Replaces a wallbox of a draft offer with what you send; fields you leave out go back to their default. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "25"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The wallbox's material id (`materialWallboxId`); a wallbox is on an offer at most once."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferWallboxInput"
              },
              "examples": {
                "example": {
                  "summary": "The wallbox after the change",
                  "value": {
                    "materialWallboxId": "25",
                    "netPrice": 790,
                    "active": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The wallbox after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferWallbox"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a wallbox from an offer",
        "tags": [
          "Offer wallboxes"
        ],
        "description": "Removes a wallbox from a draft offer. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "25"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The wallbox's material id (`materialWallboxId`); a wallbox is on an offer at most once."
          }
        ],
        "responses": {
          "204": {
            "description": "The wallbox was removed."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `in_use`: the line is part of what the customer was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/misc": {
      "get": {
        "summary": "List the misc lines of an offer",
        "tags": [
          "Offer misc materials"
        ],
        "description": "Returns all misc lines of an offer. Requires the `read:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer's misc lines; an empty list for an unknown offer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OfferMisc"
                      },
                      "description": "The offer's misc lines."
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a misc line to an offer",
        "tags": [
          "Offer misc materials"
        ],
        "description": "Adds a misc line to a draft offer. The offer's total price is updated. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferMiscInput"
              },
              "examples": {
                "example": {
                  "summary": "Add a misc line",
                  "value": {
                    "materialMiscId": "8",
                    "netPrice": 3.5,
                    "piecesCount": 40
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new misc line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferMisc"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `already_exists`: this misc line is on the offer already (batteries and wallboxes only).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{offerId}/misc/{itemId}": {
      "put": {
        "summary": "Change a misc line on an offer",
        "tags": [
          "Offer misc materials"
        ],
        "description": "Replaces a misc line of a draft offer with what you send; fields you leave out go back to their default. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "640"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/misc`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OfferMiscInput"
              },
              "examples": {
                "example": {
                  "summary": "The misc line after the change",
                  "value": {
                    "materialMiscId": "8",
                    "netPrice": 3.5,
                    "piecesCount": 40
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The misc line after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferMisc"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a misc line from an offer",
        "tags": [
          "Offer misc materials"
        ],
        "description": "Removes a misc line from a draft offer. Requires the `write:offers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "offerId",
            "in": "path",
            "description": "The offer's id."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "640"
            },
            "required": true,
            "name": "itemId",
            "in": "path",
            "description": "The line's `id`, as listed by `GET /v1/offers/{offerId}/misc`."
          }
        ],
        "responses": {
          "204": {
            "description": "The misc line was removed."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`offer_not_draft`: the offer was published already; its lines can no longer change. Create a new offer instead. `in_use`: the line is part of what the customer was shown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/customers": {
      "get": {
        "summary": "List customers",
        "description": "Returns your customers, newest first. Filter with `name`, `email` and `city`, sort with `sort` (e.g. `sort=lastName.asc`) and page with `limit` and `offset`. Requires the `read:customers` scope.",
        "tags": [
          "Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, firstName, lastName, fullName, email, projectsCount, offersCount."
          },
          {
            "schema": {
              "type": "string",
              "example": "Müller"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only customers whose full name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "@example.com"
            },
            "required": false,
            "name": "email",
            "in": "query",
            "description": "Only customers whose email address contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "München"
            },
            "required": false,
            "name": "city",
            "in": "query",
            "description": "Only customers whose city contains this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of customers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a customer",
        "description": "Creates a customer. Only `firstName` is required. The response contains the new customer's `id`, which you need to create a project for them. Requires the `write:customers` scope.",
        "tags": [
          "Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerInput"
              },
              "examples": {
                "complete": {
                  "summary": "Customer with contact details and address",
                  "value": {
                    "firstName": "Anna",
                    "lastName": "Müller",
                    "email": "anna.mueller@example.com",
                    "phoneNumber": "+49 89 1234567",
                    "gender": "FEMALE",
                    "address": {
                      "street": "Lindenstraße",
                      "streetNumber": "12",
                      "zip": "80331",
                      "city": "München"
                    }
                  }
                },
                "minimal": {
                  "summary": "Only the required field",
                  "value": {
                    "firstName": "Anna"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/customers/{id}": {
      "get": {
        "summary": "Get a customer",
        "description": "Returns one customer. Requires the `read:customers` scope.",
        "tags": [
          "Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "77"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The customer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a customer",
        "description": "Changes a customer. Send only the fields to change; the others keep their value. `null` clears a field. Requires the `write:customers` scope.",
        "tags": [
          "Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "77"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The customer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerUpdate"
              },
              "examples": {
                "newEmail": {
                  "summary": "New email address",
                  "value": {
                    "email": "anna@mueller-family.example"
                  }
                },
                "moved": {
                  "summary": "Moved house",
                  "value": {
                    "address": {
                      "street": "Hauptstraße",
                      "streetNumber": "3a",
                      "zip": "85221",
                      "city": "Dachau"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The customer after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Customer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a customer",
        "description": "Deletes a customer who has no projects. Delete the customer's projects first (`DELETE /v1/projects/{id}`); that also deletes their offers. Requires the `write:customers` scope.",
        "tags": [
          "Customers"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "77"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The customer's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The customer was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the customer still has projects. Delete them first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offer-requests": {
      "get": {
        "summary": "List offer requests",
        "description": "Returns the requests (Anfragen) customers sent you, usually through your configurator, newest first. Filter with `name`, `state` and `city`, page with `limit` and `offset`. Requires the `read:offers` scope.",
        "tags": [
          "Offer requests"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, state, offersCount."
          },
          {
            "schema": {
              "type": "string",
              "example": "Müller"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only requests whose customer's full name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "REQUIRED_DATA_UPLOADED"
            },
            "required": false,
            "name": "state",
            "in": "query",
            "description": "Only requests in this state; see `state` in the response for the values."
          },
          {
            "schema": {
              "type": "string",
              "example": "München"
            },
            "required": false,
            "name": "city",
            "in": "query",
            "description": "Only requests whose site is in a city containing this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of offer requests.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferRequestList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offer-requests/{id}": {
      "get": {
        "summary": "Get an offer request",
        "description": "Returns one offer request (Anfrage) with the customer's contact details. Requires the `read:offers` scope.",
        "tags": [
          "Offer requests"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "501"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer request's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The offer request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OfferRequest"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials": {
      "get": {
        "summary": "Find materials by key across all types",
        "description": "Finds products by article number without knowing their type: by manufacturer article number (add `manufacturerId` when two manufacturers use the same number), by your own article number, or by a vendor's article number (`vendorName` and `vendorArticleNumber`, matched against the current purchase price). All material types are searched, and each match says its `type`. Send at least one of the three article numbers; `name` and `archived` narrow the result. Requires the `read:materials` scope.",
        "tags": [
          "Material catalog"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "The matching materials, of any type.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: none of the three article numbers was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/purchase-prices/import": {
      "post": {
        "summary": "Import a vendor price list",
        "description": "Updates purchase prices for many products in one call, matching each row to a product by the vendor's article number first and the manufacturer article number second. Only active products match. Each matched row updates the product's purchase price from `vendorName` — or adds one when the product has no price from this vendor yet — records the vendor article number on it, so the next list matches directly, and makes it the product's current price. Rows are processed independently — one failing row does not roll back the others; the response says per row what happened. Use `dryRun` to see the matching before writing. Requires the `write:materials` scope.",
        "tags": [
          "Material catalog"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PurchasePriceImport"
              },
              "examples": {
                "check": {
                  "summary": "Check the matching first (dry run)",
                  "value": {
                    "vendorName": "Solar-Großhandel Schmidt",
                    "dryRun": true,
                    "rows": [
                      {
                        "vendorArticleNumber": "A09402",
                        "manufacturerArticleNumber": "A-MAH54Mb-445",
                        "pricePerPiece": 98.5
                      },
                      {
                        "vendorArticleNumber": "W-11873",
                        "manufacturerArticleNumber": "SUN2000-10KTL-M1",
                        "pricePerPiece": 1499
                      }
                    ]
                  }
                },
                "import": {
                  "summary": "Import the prices",
                  "value": {
                    "vendorName": "Solar-Großhandel Schmidt",
                    "rows": [
                      {
                        "vendorArticleNumber": "A09402",
                        "manufacturerArticleNumber": "A-MAH54Mb-445",
                        "pricePerPiece": 98.5
                      },
                      {
                        "vendorArticleNumber": "W-11873",
                        "manufacturerArticleNumber": "SUN2000-10KTL-M1",
                        "pricePerPiece": 1499
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What happened to each row, and a count per outcome.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchasePriceImportResult"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/pv-modules": {
      "get": {
        "summary": "List PV modules",
        "description": "Returns the PV modules in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice, nominalPowerW."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of PV modules.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a PV module",
        "description": "Adds a PV module to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PvModuleInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new PV module.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a PV module by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges a PV module. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PvModuleUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The PV module after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a PV module by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes a PV module from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The PV module was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/pv-modules/{id}": {
      "get": {
        "summary": "Get a PV module",
        "description": "Returns one PV module. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The PV module's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The PV module.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a PV module",
        "description": "Changes a PV module. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/pv-modules?manufacturerArticleNumber=…`.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The PV module's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PvModuleUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The PV module after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a PV module",
        "description": "Deletes a PV module from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/pv-modules?manufacturerArticleNumber=…`.",
        "tags": [
          "PV modules"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The PV module's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The PV module was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/batteries": {
      "get": {
        "summary": "List batteries",
        "description": "Returns the batteries in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice, capacityKwh."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of batteries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a battery",
        "description": "Adds a battery to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatteryInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new battery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a battery by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges a battery. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatteryUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The battery after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a battery by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes a battery from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The battery was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/batteries/{id}": {
      "get": {
        "summary": "Get a battery",
        "description": "Returns one battery. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The battery's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The battery.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a battery",
        "description": "Changes a battery. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/batteries?manufacturerArticleNumber=…`.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The battery's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatteryUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The battery after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a battery",
        "description": "Deletes a battery from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/batteries?manufacturerArticleNumber=…`.",
        "tags": [
          "Batteries"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The battery's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The battery was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/inverters": {
      "get": {
        "summary": "List inverters",
        "description": "Returns the inverters in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice, acNominalPowerKw."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of inverters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an inverter",
        "description": "Adds an inverter to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Send its MPP trackers in `mppTrackers`, without ids: offers wire the module strings to them. Requires the `write:materials` scope.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InverterInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new inverter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an inverter by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges an inverter. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To change the MPP trackers, send `mppTrackers` with all of them: a tracker with an `id` is changed, one without is added, and the others are deleted. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InverterUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                },
                "mppTrackers": {
                  "summary": "Set the MPP trackers",
                  "description": "Keeps tracker 141 with these values, adds a second tracker and deletes the inverter's other trackers.",
                  "value": {
                    "mppTrackers": [
                      {
                        "id": "141",
                        "inputsCount": 2,
                        "maxInputCurrentA": 13.5,
                        "maxShortCircuitCurrentA": 16.9,
                        "maxInputPowerKw": 7.5
                      },
                      {
                        "inputsCount": 2,
                        "maxInputCurrentA": 13.5,
                        "maxShortCircuitCurrentA": 16.9,
                        "maxInputPowerKw": 7.5
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The inverter after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number. `in_use`: an offer uses an MPP tracker that `mppTrackers` leaves out. Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an inverter by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes an inverter from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The inverter was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/inverters/{id}": {
      "get": {
        "summary": "Get an inverter",
        "description": "Returns one inverter. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The inverter's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The inverter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an inverter",
        "description": "Changes an inverter. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To change the MPP trackers, send `mppTrackers` with all of them: a tracker with an `id` is changed, one without is added, and the others are deleted. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/inverters?manufacturerArticleNumber=…`.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The inverter's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InverterUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                },
                "mppTrackers": {
                  "summary": "Set the MPP trackers",
                  "description": "Keeps tracker 141 with these values, adds a second tracker and deletes the inverter's other trackers.",
                  "value": {
                    "mppTrackers": [
                      {
                        "id": "141",
                        "inputsCount": 2,
                        "maxInputCurrentA": 13.5,
                        "maxShortCircuitCurrentA": 16.9,
                        "maxInputPowerKw": 7.5
                      },
                      {
                        "inputsCount": 2,
                        "maxInputCurrentA": 13.5,
                        "maxShortCircuitCurrentA": 16.9,
                        "maxInputPowerKw": 7.5
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The inverter after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number. `in_use`: an offer uses an MPP tracker that `mppTrackers` leaves out. Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an inverter",
        "description": "Deletes an inverter from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/inverters?manufacturerArticleNumber=…`.",
        "tags": [
          "Inverters"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The inverter's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The inverter was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/wallboxes": {
      "get": {
        "summary": "List wallboxes",
        "description": "Returns the wallboxes in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of wallboxes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a wallbox",
        "description": "Adds a wallbox to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WallboxInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new wallbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a wallbox by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges a wallbox. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WallboxUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The wallbox after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a wallbox by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes a wallbox from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The wallbox was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/wallboxes/{id}": {
      "get": {
        "summary": "Get a wallbox",
        "description": "Returns one wallbox. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The wallbox's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The wallbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a wallbox",
        "description": "Changes a wallbox. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/wallboxes?manufacturerArticleNumber=…`.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The wallbox's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WallboxUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The wallbox after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a wallbox",
        "description": "Deletes a wallbox from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/wallboxes?manufacturerArticleNumber=…`.",
        "tags": [
          "Wallboxes"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The wallbox's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The wallbox was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/equipment": {
      "get": {
        "summary": "List equipment",
        "description": "Returns the equipment in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of equipment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an equipment item",
        "description": "Adds an equipment item to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EquipmentInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new equipment item.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an equipment item by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges an equipment item. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EquipmentUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The equipment item after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an equipment item by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes an equipment item from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The equipment item was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/equipment/{id}": {
      "get": {
        "summary": "Get an equipment item",
        "description": "Returns one equipment item. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The equipment item's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The equipment item.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an equipment item",
        "description": "Changes an equipment item. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/equipment?manufacturerArticleNumber=…`.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The equipment item's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EquipmentUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The equipment item after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an equipment item",
        "description": "Deletes an equipment item from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/equipment?manufacturerArticleNumber=…`.",
        "tags": [
          "Equipment"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The equipment item's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The equipment item was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/misc": {
      "get": {
        "summary": "List misc materials",
        "description": "Returns the misc materials in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of misc materials.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a misc material",
        "description": "Adds a misc material to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MiscMaterialInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new misc material.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a misc material by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges a misc material. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MiscMaterialUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The misc material after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a misc material by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes a misc material from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The misc material was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/misc/{id}": {
      "get": {
        "summary": "Get a misc material",
        "description": "Returns one misc material. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The misc material's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The misc material.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a misc material",
        "description": "Changes a misc material. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/misc?manufacturerArticleNumber=…`.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The misc material's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MiscMaterialUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The misc material after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a misc material",
        "description": "Deletes a misc material from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/misc?manufacturerArticleNumber=…`.",
        "tags": [
          "Misc materials"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The misc material's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The misc material was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/subconstruction": {
      "get": {
        "summary": "List mounting systems",
        "description": "Returns the mounting systems in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of mounting systems.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a mounting system",
        "description": "Adds a mounting system to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubconstructionInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new mounting system.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a mounting system by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges a mounting system. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubconstructionUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The mounting system after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a mounting system by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes a mounting system from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The mounting system was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/subconstruction/{id}": {
      "get": {
        "summary": "Get a mounting system",
        "description": "Returns one mounting system. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The mounting system's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The mounting system.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a mounting system",
        "description": "Changes a mounting system. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/subconstruction?manufacturerArticleNumber=…`.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The mounting system's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubconstructionUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The mounting system after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a mounting system",
        "description": "Deletes a mounting system from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/subconstruction?manufacturerArticleNumber=…`.",
        "tags": [
          "Subconstruction"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The mounting system's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The mounting system was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/emergency-powers": {
      "get": {
        "summary": "List emergency power products",
        "description": "Returns the emergency power products in your catalog, most recently changed first. Filter with `name`, `manufacturerId`, `archived` or one of the article numbers, sort with `sort` and page with `limit` and `offset`. The technical data is in `specs`. Requires the `read:materials` scope.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, updatedAt, name, purchasePrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Neostar"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only materials whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only materials of this manufacturer."
          },
          {
            "schema": {
              "type": "string",
              "example": "A-MAH54Mb-445"
            },
            "required": false,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "Only materials with this manufacturer article number (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "PV-0042"
            },
            "required": false,
            "name": "internalArticleNumber",
            "in": "query",
            "description": "Only the material with this article number of yours (exact, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "Solar-Großhandel Schmidt"
            },
            "required": false,
            "name": "vendorName",
            "in": "query",
            "description": "Only materials whose current purchase price is from this vendor (exact name, any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "A09402"
            },
            "required": false,
            "name": "vendorArticleNumber",
            "in": "query",
            "description": "Only materials whose current purchase price carries this vendor article number (exact, any case). Use it together with `vendorName`."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "false"
            },
            "required": false,
            "name": "archived",
            "in": "query",
            "description": "`true` for archived materials only, `false` for active ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of emergency power products.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MaterialList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create an emergency power product",
        "description": "Adds an emergency power product to your catalog. Send `purchasePrice` with what you pay for it; the selling prices (`sellingPrices`) follow from it and the margins of your company sites. Requires the `write:materials` scope.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmergencyPowerMaterialInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new emergency power product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an emergency power product by manufacturer article number",
        "description": "Same as updating by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Handy for syncing a price list, which carries these numbers but not our ids. Only active products are considered.\n\nChanges an emergency power product. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmergencyPowerMaterialUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The emergency power product after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id. `duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an emergency power product by manufacturer article number",
        "description": "Same as deleting by id, but the product is found by its `manufacturerArticleNumber` (add `manufacturerId` when two manufacturers use the same number). Only active products are considered.\n\nDeletes an emergency power product from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "minLength": 1,
              "example": "A-MAH54Mb-445"
            },
            "required": true,
            "name": "manufacturerArticleNumber",
            "in": "query",
            "description": "The product's manufacturer article number (exact, any case). Only active products are considered."
          },
          {
            "schema": {
              "type": "string",
              "example": "12"
            },
            "required": false,
            "name": "manufacturerId",
            "in": "query",
            "description": "Only consider this manufacturer's products. Needed when two manufacturers use the same number; otherwise the call answers `409 ambiguous_key`."
          }
        ],
        "responses": {
          "204": {
            "description": "The emergency power product was deleted."
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: no active product has this manufacturer article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead. `ambiguous_key`: several active products have this manufacturer article number; `message` lists their ids. Add `manufacturerId` or use the id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/materials/emergency-powers/{id}": {
      "get": {
        "summary": "Get an emergency power product",
        "description": "Returns one emergency power product. To find it by manufacturer article number, use the list with `?manufacturerArticleNumber=`. Requires the `read:materials` scope.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The emergency power product's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The emergency power product.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update an emergency power product",
        "description": "Changes an emergency power product. Send only the fields to change; the others keep their value. For a new purchase price send `purchasePrice` with `pricePerPiece` and `vendorName`: the price this vendor already has for the product is updated (a new vendor gets one) and becomes the current price. To take the product off new offers without deleting it, send `archived: true`. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/emergency-powers?manufacturerArticleNumber=…`.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The emergency power product's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EmergencyPowerMaterialUpdate"
              },
              "examples": {
                "newPrice": {
                  "summary": "New purchase price",
                  "value": {
                    "purchasePrice": {
                      "pricePerPiece": 105.5,
                      "vendorName": "Solar-Großhandel Schmidt"
                    }
                  }
                },
                "archive": {
                  "summary": "Archive the product",
                  "value": {
                    "archived": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The emergency power product after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Material"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_internal_article_number`: another active product already has this internal article number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an emergency power product",
        "description": "Deletes an emergency power product from your catalog. A product that is on an offer cannot be deleted; archive it instead with `archived: true`: it stays on existing offers but can no longer be added to new ones. Requires the `write:materials` scope. To find the product by its manufacturer article number instead of the id, call the same method on `/v1/materials/emergency-powers?manufacturerArticleNumber=…`.",
        "tags": [
          "Emergency power"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "150"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The emergency power product's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The emergency power product was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the product is on an offer. Archive it instead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/manufacturers": {
      "get": {
        "summary": "List manufacturers",
        "tags": [
          "Manufacturers"
        ],
        "description": "Returns the manufacturers of your catalog, sorted by name. Filter with `name` and page with `limit` and `offset`. Requires the `read:manufacturers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name."
          },
          {
            "schema": {
              "type": "string",
              "example": "Aiko"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only manufacturers whose name contains this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of manufacturers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManufacturerList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a manufacturer",
        "tags": [
          "Manufacturers"
        ],
        "description": "Adds a manufacturer, so you can add their products to the catalog. Only `name` is required. Requires the `write:manufacturers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManufacturerInput"
              },
              "examples": {
                "example": {
                  "summary": "A new manufacturer",
                  "value": {
                    "name": "Aiko",
                    "websiteUrl": "https://aikosolar.com",
                    "contactEmail": "sales@aikosolar.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new manufacturer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Manufacturer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/manufacturers/{id}": {
      "get": {
        "summary": "Get a manufacturer",
        "tags": [
          "Manufacturers"
        ],
        "description": "Returns one manufacturer. Requires the `read:manufacturers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "12"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The manufacturer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The manufacturer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Manufacturer"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a manufacturer",
        "tags": [
          "Manufacturers"
        ],
        "description": "Changes a manufacturer. Send only the fields to change. Requires the `write:manufacturers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "12"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The manufacturer's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManufacturerUpdate"
              },
              "examples": {
                "example": {
                  "summary": "New contact address",
                  "value": {
                    "contactEmail": "vertrieb@aikosolar.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The manufacturer after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Manufacturer"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a manufacturer",
        "tags": [
          "Manufacturers"
        ],
        "description": "Deletes a manufacturer that has no products in your catalog. Requires the `write:manufacturers` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "12"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The manufacturer's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The manufacturer was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: products in your catalog still belong to this manufacturer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/services": {
      "get": {
        "summary": "List services",
        "tags": [
          "Services"
        ],
        "description": "Returns the services you offer, sorted by name. Filter with `name` and `includedInOffer`, page with `limit` and `offset`. Requires the `read:services` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name, fixedPrice."
          },
          {
            "schema": {
              "type": "string",
              "example": "Gerüst"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only services whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "true"
            },
            "required": false,
            "name": "includedInOffer",
            "in": "query",
            "description": "`true` for services put on new offers automatically, `false` for the others."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of services.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a service",
        "tags": [
          "Services"
        ],
        "description": "Adds a service you can put on offers. Prices are net. Requires the `write:services` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceInput"
              },
              "examples": {
                "example": {
                  "summary": "Scaffolding at a flat price",
                  "value": {
                    "name": "Gerüst",
                    "description": "Gerüst bis 8 m Traufhöhe, inkl. Auf- und Abbau.",
                    "includedInOffer": true,
                    "fixedPrice": 450,
                    "pricePerModule": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/services/{id}": {
      "get": {
        "summary": "Get a service",
        "tags": [
          "Services"
        ],
        "description": "Returns one service. Requires the `read:services` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "5"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The service's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a service",
        "tags": [
          "Services"
        ],
        "description": "Changes a service. Send only the fields to change. Offers that already have the service keep their price. Requires the `write:services` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "5"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The service's id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ServiceUpdate"
              },
              "examples": {
                "example": {
                  "summary": "New price",
                  "value": {
                    "fixedPrice": 490
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The service after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Service"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a service",
        "tags": [
          "Services"
        ],
        "description": "Deletes a service that is not in use. Requires the `write:services` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "5"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The service's id."
          }
        ],
        "responses": {
          "204": {
            "description": "The service was deleted."
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`in_use`: the service is still in use, for example on an offer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/company/members": {
      "get": {
        "summary": "List team members",
        "tags": [
          "Company"
        ],
        "description": "Returns the members of your team, sorted by name. A member's `id` is what projects and offers take as `associatedCompanyMemberId` and bookings as `plannerMemberId`. Filter with `name` and `siteId`, page with `limit` and `offset`. Requires the `read:company` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "name.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: name, createdAt."
          },
          {
            "schema": {
              "type": "string",
              "example": "Max"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only members whose name contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "1"
            },
            "required": false,
            "name": "siteId",
            "in": "query",
            "description": "Only members of this company site."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of team members.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanyMemberList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/company/sites": {
      "get": {
        "summary": "List company sites",
        "tags": [
          "Company"
        ],
        "description": "Returns the sites (Standorte) of your company, sorted by name. A site's `id` is what projects take as `associatedCompanySiteId`; the site's margins and VAT rate decide the prices of the project's offers, and every material lists its price per site in `sellingPrices`. Requires the `read:company` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "name.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: name, createdAt."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of sites.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompanySiteList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/company": {
      "get": {
        "summary": "Get your company",
        "tags": [
          "Company"
        ],
        "description": "Returns the profile of the company the API key belongs to. Requires the `read:company` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Company"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update your company",
        "tags": [
          "Company"
        ],
        "description": "Changes your company profile. Send only the fields to change. Requires the `write:company` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompanyInput"
              },
              "examples": {
                "example": {
                  "summary": "New support contact",
                  "value": {
                    "supportEmail": "kundenservice@solario.example",
                    "supportPhoneNumber": "+49 30 7654321"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your company after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Company"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/offers": {
      "get": {
        "summary": "Offers over time",
        "description": "How many offers were created per day, week or month (`interval`). The whole period from `from` to `to` comes in one response, without paging; without them you get the last 12 months. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-01-01"
            },
            "required": false,
            "name": "from",
            "in": "query",
            "description": "First day to include (YYYY-MM-DD, German calendar). Default: 12 months before `to`."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-12-31"
            },
            "required": false,
            "name": "to",
            "in": "query",
            "description": "Last day to include (YYYY-MM-DD, German calendar). Default: today."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "daily",
                "weekly",
                "monthly"
              ],
              "default": "monthly",
              "example": "monthly"
            },
            "required": false,
            "name": "interval",
            "in": "query",
            "description": "One point per `daily`, `weekly` (from Monday) or `monthly` period. Default `monthly`."
          }
        ],
        "responses": {
          "200": {
            "description": "One point per period, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsSeries"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/offer-requests": {
      "get": {
        "summary": "Offer requests over time",
        "description": "How many offer requests came in per day, week or month (`interval`). The whole period from `from` to `to` comes in one response, without paging; without them you get the last 12 months. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-01-01"
            },
            "required": false,
            "name": "from",
            "in": "query",
            "description": "First day to include (YYYY-MM-DD, German calendar). Default: 12 months before `to`."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-12-31"
            },
            "required": false,
            "name": "to",
            "in": "query",
            "description": "Last day to include (YYYY-MM-DD, German calendar). Default: today."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "daily",
                "weekly",
                "monthly"
              ],
              "default": "monthly",
              "example": "monthly"
            },
            "required": false,
            "name": "interval",
            "in": "query",
            "description": "One point per `daily`, `weekly` (from Monday) or `monthly` period. Default `monthly`."
          }
        ],
        "responses": {
          "200": {
            "description": "One point per period, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsSeries"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/conversions": {
      "get": {
        "summary": "Request conversion rates",
        "description": "How many offer requests led to an accepted indication offer, an accepted detailed offer, or ran out without one (`type`). Per day, `count` is how many requests came in and `converted` how many of them converted, so the rate is `converted / count`. The whole period comes in one response. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-01-01"
            },
            "required": false,
            "name": "from",
            "in": "query",
            "description": "First day to include (YYYY-MM-DD, German calendar). Default: 12 months before `to`."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-12-31"
            },
            "required": false,
            "name": "to",
            "in": "query",
            "description": "Last day to include (YYYY-MM-DD, German calendar). Default: today."
          },
          {
            "schema": {
              "type": "string",
              "enum": [
                "indication",
                "detailed",
                "expired"
              ],
              "default": "detailed",
              "example": "detailed"
            },
            "required": false,
            "name": "type",
            "in": "query",
            "description": "`detailed` (the default) or `indication`: requests with an accepted offer of that kind. `expired`: requests whose offers all ran out without one being accepted."
          }
        ],
        "responses": {
          "200": {
            "description": "One point per day, oldest first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsConversions"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/demographics": {
      "get": {
        "summary": "Customers by salutation",
        "description": "How many customers you have, and how many of them have the salutation Herr or Frau. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The counts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsDemographics"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/product-sales": {
      "get": {
        "summary": "Product sales",
        "description": "What you sold, per product and day, newest first. A product counts as sold when a detailed offer with it is accepted. Filter with `category` and `materialId`, choose the period with `from` and `to` (default: the last 12 months), page with `limit` and `offset`. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "date.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: date, soldUnits, soldOffersCount, productName."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-01-01"
            },
            "required": false,
            "name": "from",
            "in": "query",
            "description": "First day to include (YYYY-MM-DD, German calendar). Default: 12 months before `to`."
          },
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
              "example": "2026-12-31"
            },
            "required": false,
            "name": "to",
            "in": "query",
            "description": "Last day to include (YYYY-MM-DD, German calendar). Default: today."
          },
          {
            "schema": {
              "type": "string",
              "example": "BATTERY"
            },
            "required": false,
            "name": "category",
            "in": "query",
            "description": "Only this product type: `PV_MODULE`, `INVERTER`, `BATTERY`, `WALLBOX`, `SUBCONSTRUCTION`, `EQUIPMENT`, `MISC` or `EMERGENCY_POWER`."
          },
          {
            "schema": {
              "type": "string",
              "example": "88"
            },
            "required": false,
            "name": "materialId",
            "in": "query",
            "description": "Only this material. Ids are unique per type only, so send `category` too."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of sales.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsProductSaleList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/analytics/open-offers": {
      "get": {
        "summary": "Products in open offers",
        "description": "What is in offers that are still open, per product, most units first: your pipeline, useful to plan purchases before anything is accepted. Filter with `category` and `materialId`, page with `limit` and `offset`. Requires the `read:analytics` scope.",
        "tags": [
          "Analytics"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "openUnits.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: openUnits, openOffersCount, productName."
          },
          {
            "schema": {
              "type": "string",
              "example": "BATTERY"
            },
            "required": false,
            "name": "category",
            "in": "query",
            "description": "Only this product type: `PV_MODULE`, `INVERTER`, `BATTERY`, `WALLBOX`, `SUBCONSTRUCTION`, `EQUIPMENT`, `MISC` or `EMERGENCY_POWER`."
          },
          {
            "schema": {
              "type": "string",
              "example": "88"
            },
            "required": false,
            "name": "materialId",
            "in": "query",
            "description": "Only this material. Ids are unique per type only, so send `category` too."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of products.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalyticsOpenOfferList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{id}/network-carrier-documents/options": {
      "get": {
        "summary": "List grid-operator document options for an offer",
        "description": "Everything `POST /v1/offers/{id}/network-carrier-documents` can reference for this offer, resolved in one call: the grid operator set on its project and that operator's forms, the electrical contractors you can name, the offer's module configurations and its batteries — each flagged with what the planner would preselect.\n\nUse it to fill a document request without guessing ids; for the operator catalog on its own see `GET /v1/grid-operators`. Requires the `read:documents` scope.",
        "tags": [
          "Documents"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The ids and names this offer's document request can use.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NetworkCarrierDocumentOptions"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{id}/network-carrier-documents": {
      "post": {
        "summary": "Generate grid-operator documents",
        "description": "Fills in the grid operator's (Netzbetreiber) registration forms for an offer and returns them as a ZIP archive. The documents are rendered on demand from the offer's current technical data and are not stored, so each call produces a fresh archive.\n\nEvery field of the body is optional. With none of them, the operator set on the offer's project is used and all of its forms are produced — so `POST` with `{}` is the normal call. To pick specific forms, or an operator other than the project's, look them up with `GET /v1/grid-operators`; `GET /v1/offers/{id}/network-carrier-documents/options` resolves everything this offer can reference in one call.\n\nThis is a `POST` because the request carries a body, but it changes nothing. Requires the `read:documents` scope.",
        "tags": [
          "Documents"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/NetworkCarrierDocumentsInput"
              },
              "examples": {
                "everything": {
                  "summary": "All forms of the project's grid operator",
                  "value": {}
                },
                "chosen": {
                  "summary": "Chosen forms, with commissioning date, contractor and datasheets",
                  "value": {
                    "documentIds": [
                      "1",
                      "4"
                    ],
                    "plannedGoingLiveDate": "2026-09-01",
                    "electronicsFirmId": "3",
                    "includeDatasheets": true,
                    "includeMergedDocument": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A ZIP archive with one PDF per requested document, plus the merged PDF when asked for.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body is not valid. `network_carrier_not_set`: the project has no grid operator; send `networkCarrierId` or set one on the project. `no_documents_configured`: the grid operator has no forms set up. `unknown_document_ids`: a `documentIds` entry is not one of the operator's forms; `message` lists the valid ones.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`generation_failed`: the documents could not be produced; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/offers/{id}/string-plan": {
      "get": {
        "summary": "Generate the string plan",
        "description": "Renders the offer's string plan — how the modules are wired into the inverters' MPP trackers — as a PDF. Generated on demand from the offer's current configuration and not stored, so each call reflects the offer as it is now. Requires the `read:documents` scope.",
        "tags": [
          "Documents"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "812"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The offer's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The string plan as a PDF.",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`generation_failed`: the documents could not be produced; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings": {
      "get": {
        "summary": "List bookings",
        "description": "Returns your appointments, latest start first. Filter with `status`, `paymentStatus`, `source`, `typeSlug`, `customer` and the period `from`/`to`; sort with `sort` and page with `limit` and `offset`. Phone, address, billing details and notes are only filled in for an API key that also has `read:customers`. Requires the `read:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "startsAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: startsAt, createdAt, status, paymentStatus, price."
          },
          {
            "schema": {
              "type": "string",
              "example": "CONFIRMED"
            },
            "required": false,
            "name": "status",
            "in": "query",
            "description": "Only bookings with this status: `PENDING_PAYMENT`, `CONFIRMED`, `CANCELLED`, `COMPLETED`, `NO_SHOW` or `EXPIRED`."
          },
          {
            "schema": {
              "type": "string",
              "example": "UNPAID"
            },
            "required": false,
            "name": "paymentStatus",
            "in": "query",
            "description": "Only bookings with this payment status: `FREE`, `UNPAID`, `PAID` or `REFUNDED`."
          },
          {
            "schema": {
              "type": "string",
              "example": "BUILDER"
            },
            "required": false,
            "name": "source",
            "in": "query",
            "description": "Only bookings from here: `BUILDER`, `BOOKING_PAGE` or `PLANNER`."
          },
          {
            "schema": {
              "type": "string",
              "example": "erstgespraech"
            },
            "required": false,
            "name": "typeSlug",
            "in": "query",
            "description": "Only bookings of this appointment type."
          },
          {
            "schema": {
              "type": "string",
              "example": "Müller"
            },
            "required": false,
            "name": "customer",
            "in": "query",
            "description": "Only bookings whose customer name or email address contains this text (any case)."
          },
          {
            "schema": {
              "type": "string",
              "example": "2026-09-01T00:00:00+02:00"
            },
            "required": false,
            "name": "from",
            "in": "query",
            "description": "Only appointments starting at or after this time (ISO 8601)."
          },
          {
            "schema": {
              "type": "string",
              "example": "2026-10-01T00:00:00+02:00"
            },
            "required": false,
            "name": "to",
            "in": "query",
            "description": "Only appointments starting before this time (ISO 8601)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of bookings.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BookingList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a booking",
        "description": "Books an appointment for a customer, as if your team entered it in the planner (`source: PLANNER`). It is confirmed at once and the customer gets the usual confirmation email. No payment is taken: a paid appointment type stays `UNPAID` until you mark it paid (`POST /v1/bookings/{id}/mark-paid`). `startsAt` must be a free time of that type; otherwise the call answers `409 slot_unavailable`. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingInput"
              },
              "examples": {
                "example": {
                  "summary": "Book a first meeting",
                  "value": {
                    "typeSlug": "erstgespraech",
                    "startsAt": "2026-09-15T10:00:00+02:00",
                    "customerName": "Anna Müller",
                    "customerEmail": "anna.mueller@example.com",
                    "customerPhone": "+49 89 1234567"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no appointment type with this `typeSlug`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`slot_unavailable`: the time is not free. `payment_not_configured`: the appointment type costs money, but your company has no Stripe connection.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}": {
      "get": {
        "summary": "Get a booking",
        "description": "Returns one booking. Phone, address, billing details and notes are only filled in for an API key that also has `read:customers`. Requires the `read:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "responses": {
          "200": {
            "description": "The booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/appointment-types": {
      "get": {
        "summary": "List appointment types",
        "description": "Returns the kinds of appointment your customers can book. Use a type's `slug` as `typeSlug` when booking. Requires the `read:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ],
              "example": "true"
            },
            "required": false,
            "name": "active",
            "in": "query",
            "description": "`true` for bookable types only, `false` for inactive ones only."
          }
        ],
        "responses": {
          "200": {
            "description": "The appointment types.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppointmentTypeList"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/follow-up": {
      "post": {
        "summary": "Book a follow-up",
        "description": "Books the next appointment for the customer of an existing booking. Name, contact details and offer request come from that booking, so you only say when, and if it should differ, which appointment type and which team member. A follow-up is confirmed at once, counts as entered by your team (`source: PLANNER`), is free of charge whatever its type costs, and names the booking it follows in `followUpOf`. The customer gets the usual confirmation email. Only a confirmed, completed or no-show booking can be followed up. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingFollowUpInput"
              },
              "examples": {
                "example": {
                  "summary": "Follow-up two weeks later, other type",
                  "value": {
                    "startsAt": "2026-09-29T10:00:00+02:00",
                    "typeSlug": "detailplanung"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The new follow-up booking.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such booking in your company, or no appointment type with this `typeSlug`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`slot_unavailable`: the time is not free. `follow_up_not_allowed`: only a confirmed, completed or no-show booking can be followed up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/cancel": {
      "post": {
        "summary": "Cancel a booking",
        "description": "Cancels a pending or confirmed appointment, frees the time and emails the customer. With `refund: true`, a payment made through Stripe goes back to the customer. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingCancelInput"
              },
              "examples": {
                "example": {
                  "summary": "Cancel and refund",
                  "value": {
                    "refund": true,
                    "reason": "Termin passt nicht mehr."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The booking, now cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`not_cancellable`: only a pending or confirmed booking can be cancelled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/reschedule": {
      "post": {
        "summary": "Reschedule a booking",
        "description": "Moves a confirmed appointment in the future to another time, updates the connected calendar and sends the customer a new confirmation. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BookingRescheduleInput"
              },
              "examples": {
                "example": {
                  "summary": "Move to another day",
                  "value": {
                    "startsAt": "2026-09-17T14:00:00+02:00"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The booking at its new time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`slot_unavailable`: the new time is not free. `not_reschedulable`: only a confirmed appointment in the future can be moved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/complete": {
      "post": {
        "summary": "Mark a booking completed",
        "description": "Records that the appointment took place. Only a confirmed booking changes; for any other the call changes nothing. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "responses": {
          "200": {
            "description": "The booking after the change; unchanged when it was not in the right state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict reported by the booking service; `error` and `message` say which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/no-show": {
      "post": {
        "summary": "Mark a booking as a no-show",
        "description": "Records that the customer did not come. Only a confirmed booking changes; for any other the call changes nothing. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "responses": {
          "200": {
            "description": "The booking after the change; unchanged when it was not in the right state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict reported by the booking service; `error` and `message` say which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/bookings/{id}/mark-paid": {
      "post": {
        "summary": "Mark a booking paid",
        "description": "Records that an unpaid booking was paid outside Stripe, e.g. in cash or by bank transfer. Only an `UNPAID` booking changes; for any other the call changes nothing. Requires the `write:bookings` scope.",
        "tags": [
          "Bookings"
        ],
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "example": "a0000000-0000-4000-8000-000000000001"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The booking's id (a uuid)."
          }
        ],
        "responses": {
          "200": {
            "description": "The booking after the change; unchanged when it was not in the right state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Booking"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: the body, a path or a query parameter is not valid. `message` names each problem, e.g. `firstName: Required`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A conflict reported by the booking service; `error` and `message` say which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "`booking_failed`: the booking service could not do it; `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/grid-operators": {
      "get": {
        "summary": "List grid operators",
        "tags": [
          "Grid operators"
        ],
        "description": "Returns the grid operators (Netzbetreiber) configured for your company, each with the set of registration forms attached to it. This is where the `networkCarrierId` and `documentIds` for `POST /v1/offers/{id}/network-carrier-documents` come from — `documents` is already in the order the operator expects. Requires the `read:documents` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name."
          },
          {
            "schema": {
              "type": "string",
              "example": "Stadtwerke"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only grid operators whose name contains this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "Filterable, sortable, paginated list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GridOperatorList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/grid-operators/{id}": {
      "get": {
        "summary": "Get a grid operator",
        "tags": [
          "Grid operators"
        ],
        "description": "Returns one grid operator with its registration forms, in the configured order. Requires the `read:documents` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "pattern": "^\\d+$",
              "example": "7"
            },
            "required": true,
            "name": "id",
            "in": "path",
            "description": "The grid operator's id."
          }
        ],
        "responses": {
          "200": {
            "description": "The grid operator.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/GridOperator"
                    },
                    {
                      "type": "object",
                      "description": "A grid operator (Netzbetreiber) you register installations with, together with the forms configured for it."
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`not_found`: there is no such record in your company.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/electrical-firms": {
      "get": {
        "summary": "List electrical contractors",
        "tags": [
          "Grid operators"
        ],
        "description": "Returns the electrical contractors (Elektrofachbetriebe) your company can name on grid-operator forms. Their ids are what `electronicsFirmId` takes in `POST /v1/offers/{id}/network-carrier-documents`. Requires the `read:documents` scope.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "example": 50
            },
            "required": false,
            "name": "limit",
            "in": "query",
            "description": "Page size: how many records to return, 1 to 200."
          },
          {
            "schema": {
              "type": [
                "integer",
                "null"
              ],
              "minimum": 0,
              "default": 0,
              "example": 0
            },
            "required": false,
            "name": "offset",
            "in": "query",
            "description": "How many records to skip. For the second page of 50, send `offset=50`."
          },
          {
            "schema": {
              "type": "string",
              "example": "createdAt.desc"
            },
            "required": false,
            "name": "sort",
            "in": "query",
            "description": "Sort order: a field name followed by `.asc` (ascending) or `.desc` (descending). Fields: createdAt, name."
          },
          {
            "schema": {
              "type": "string",
              "example": "Huber"
            },
            "required": false,
            "name": "name",
            "in": "query",
            "description": "Only contractors whose name contains this text (any case)."
          }
        ],
        "responses": {
          "200": {
            "description": "Filterable, sortable, paginated list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ElectricalFirmList"
                }
              }
            }
          },
          "400": {
            "description": "`validation_error`: a query parameter is not valid, e.g. `limit` above 200. `invalid_sort`: the list cannot be sorted by that field; `message` lists the fields it can.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`missing_api_key` or `invalid_api_key`: no API key was sent, or it is wrong, expired or revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope`: the API key does not have the scope this endpoint needs. `required` names it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "`db_error`: the database refused or failed the request. `message` has the details.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
