{"openapi":"3.1.0","info":{"title":"Customable Services API","version":"1.3.2","description":"Internal service hub. The Geolocation module provides German PLZ → city → street lookups plus radius/distance queries, backed by OpenPLZ + OpenStreetMap data (ODbL — attribution \"© OpenStreetMap-Mitwirkende\"). The Finance module serves German tax-office (Finanzamt) reference data plus Steuernummer validation/conversion (state form ↔ unified 13-digit). The Bank module validates IBANs (ISO 13616), enriches German IBANs with bank name + BIC, and derives a German IBAN from Bankleitzahl + Kontonummer (Bundesbank standard rule). The PDF module renders PDFs in-process (pdf-lib): fill/merge/split plus named template rendering. The Suitability module assesses a parcel's open-field PV suitability, built incrementally per data layer; a standalone PV-yield endpoint returns PVGIS annual + monthly estimates for a coordinate and system. The SEPA module generates ISO 20022 pain.001 credit-transfer files from a debtor plus a batch of payouts, and pain.008 direct-debit collection files from a creditor plus a batch of mandated debits. The Extraction module turns a document (PDF/image) plus a requested field set into structured fields with per-field confidence, provider-agnostic and processed in memory. The Parcel-locker module returns nearby parcel lockers (DHL Packstation, Amazon Locker, …) for a coordinate or PLZ, provider-agnostic and OSM-derived (ODbL — same attribution as the geo data). The Holidays module computes German public holidays plus working-day / deadline arithmetic per Bundesland, and serves community-maintained (non-authoritative) school holidays (Schulferien) per state + year. GET /api/v1/meta lists the modules/scopes and what the presented key may call. The admin surface (/api/admin/**) is session-protected and intentionally not part of this public contract.\n"},"servers":[{"url":"https://services.customable.host"}],"components":{"securitySchemes":{"sourceKey":{"type":"apiKey","in":"header","name":"x-source-key"}},"parameters":{"postalCode":{"name":"postalCode","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{5}$"},"example":"10115"}},"schemas":{"Meta":{"type":"object","additionalProperties":{"oneOf":[{"type":"number"},{"type":"string"},{"type":"boolean"},{"type":"null"}]}},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}},"CitiesResult":{"type":"object","required":["cities","state"],"properties":{"cities":{"type":"array","items":{"type":"string"}},"state":{"type":"string"}}},"RangeEntry":{"type":"object","required":["name","code","distance"],"properties":{"name":{"type":"string"},"code":{"type":"string"},"distance":{"type":"number","description":"distance in kilometres"}}},"RangeResult":{"type":"object","required":["count","citiesInRange"],"properties":{"count":{"type":"integer"},"citiesInRange":{"type":"array","items":{"$ref":"#/components/schemas/RangeEntry"}}}},"TaxOffice":{"type":"object","required":["code","name","stateCode","stateName"],"properties":{"code":{"type":"string","example":"9101"},"name":{"type":"string","example":"München"},"stateCode":{"type":"string","example":"9"},"stateName":{"type":"string","example":"Bayern"}}},"BankCode":{"type":"object","required":["code","name","bic"],"properties":{"code":{"type":"string","example":"37040044"},"name":{"type":"string","example":"Commerzbank"},"bic":{"type":["string","null"],"example":"COBADEFFXXX"}}},"IbanValidationResult":{"type":"object","required":["iban","valid","countryCode","bankCode","bankName","bic","accountNumber"],"properties":{"iban":{"type":"string","description":"formatted (4-char blocks)","example":"DE89 3704 0044 0532 0130 00"},"valid":{"type":"boolean"},"countryCode":{"type":"string","example":"DE"},"bankCode":{"type":["string","null"],"description":"German Bankleitzahl","example":"37040044"},"bankName":{"type":["string","null"],"example":"Commerzbank Köln"},"bic":{"type":["string","null"],"example":"COBADEFFXXX"},"accountNumber":{"type":["string","null"],"example":"0532013000"}}},"IbanDerivationResult":{"type":"object","required":["blz","kontonummer","iban","bankName","bic","standardRuleApplied"],"properties":{"blz":{"type":"string","example":"37040044"},"kontonummer":{"type":"string","example":"532013000"},"iban":{"type":"string","example":"DE89370400440532013000"},"bankName":{"type":["string","null"],"example":"Commerzbank Köln"},"bic":{"type":["string","null"],"example":"COBADEFFXXX"},"standardRuleApplied":{"type":"boolean","description":"Always true on a 200 — the Bundesbank standard rule was applied. ~60 institutes use non-standard \"Sonderregeln\" (gated dataset, not available to this service) not covered here; verify for those.\n"}}},"SepaParty":{"type":"object","required":["name","iban"],"properties":{"name":{"type":"string","maxLength":70,"example":"Conduvia Tenant GmbH"},"iban":{"type":"string","example":"DE89370400440532013000"},"bic":{"type":["string","null"],"pattern":"^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$","example":"COBADEFFXXX"}}},"SepaPayment":{"type":"object","required":["endToEndId","creditorName","creditorIban","amountCents","remittanceInfo"],"properties":{"endToEndId":{"type":"string","maxLength":35,"example":"payout-1"},"creditorName":{"type":"string","maxLength":70,"example":"Partner GmbH"},"creditorIban":{"type":"string","example":"DE02120300000000202051"},"creditorBic":{"type":["string","null"],"pattern":"^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"},"amountCents":{"type":"integer","minimum":1,"description":"amount in integer cents","example":12345},"remittanceInfo":{"type":"string","maxLength":140,"example":"Provision Lead 4711"}}},"SepaDirectDebitCreditor":{"type":"object","required":["name","iban","creditorIdentifier"],"properties":{"name":{"type":"string","maxLength":70,"example":"Conduvia Tenant GmbH"},"iban":{"type":"string","example":"DE89370400440532013000"},"bic":{"type":["string","null"],"pattern":"^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"},"creditorIdentifier":{"type":"string","description":"SEPA Creditor Identifier (Gläubiger-Identifikationsnummer).","example":"DE98ZZZ09999999999"}}},"SepaMandate":{"type":"object","required":["mandateId","signatureDate","sequenceType"],"properties":{"mandateId":{"type":"string","maxLength":35,"example":"mandate-42"},"signatureDate":{"type":"string","format":"date","example":"2026-01-15"},"sequenceType":{"type":"string","enum":["FRST","RCUR","OOFF","FNAL"]}}},"SepaDirectDebitTransaction":{"type":"object","required":["endToEndId","debtorName","debtorIban","amountCents","mandate","remittanceInfo"],"properties":{"endToEndId":{"type":"string","maxLength":35,"example":"collect-1"},"debtorName":{"type":"string","maxLength":70,"example":"Max Mustermann"},"debtorIban":{"type":"string","example":"DE02120300000000202051"},"debtorBic":{"type":["string","null"],"pattern":"^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"},"amountCents":{"type":"integer","minimum":1,"description":"amount in integer cents","example":4990},"mandate":{"$ref":"#/components/schemas/SepaMandate"},"remittanceInfo":{"type":"string","maxLength":140,"example":"Abo Juli 2026"}}},"ExtractionFieldSpec":{"type":"object","required":["key"],"properties":{"key":{"type":"string","maxLength":64,"example":"vorversorger"},"description":{"type":"string","maxLength":280,"description":"Optional hint guiding a vision/Document-AI provider.","example":"Name of the current energy supplier"}}},"ExtractedField":{"type":"object","required":["value","confidence","needsReview"],"properties":{"value":{"type":["string","null"]},"confidence":{"type":"number","minimum":0,"maximum":1},"needsReview":{"type":"boolean"}}},"DocumentExtractionResult":{"type":"object","required":["fields","providerReference"],"properties":{"fields":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/ExtractedField"}},"providerReference":{"type":"string","example":"stub"}}},"DetectedFormField":{"type":"object","required":["key","label","type","page","rect","confidence","source"],"properties":{"key":{"type":"string","example":"jahresverbrauch"},"label":{"type":"string","example":"Jahresverbrauch"},"type":{"type":"string","enum":["text","checkbox","signature"]},"source":{"type":"string","enum":["text-layer","ocr"],"description":"text-layer = exact; ocr = approximate (editor-refined)."},"page":{"type":"integer","description":"zero-based page index","example":0},"rect":{"type":"object","description":"PDF user-space points, bottom-left origin (pdf-lib convention).","required":["x","y","width","height"],"properties":{"x":{"type":"number"},"y":{"type":"number"},"width":{"type":"number"},"height":{"type":"number"}}},"confidence":{"type":"number","minimum":0,"maximum":1}}},"FormDetectionResult":{"type":"object","required":["pages","fields"],"properties":{"pages":{"type":"array","items":{"type":"object","required":["index","width","height"],"properties":{"index":{"type":"integer"},"width":{"type":"number"},"height":{"type":"number"}}}},"fields":{"type":"array","items":{"$ref":"#/components/schemas/DetectedFormField"}}}},"ParcelLockerStation":{"type":"object","required":["id","provider","latitude","longitude","address","source"],"properties":{"id":{"type":"string","example":"node/123456789"},"provider":{"type":"string","enum":["dhl","amazon","inpost","ups","hermes","dpd","gls","other"]},"stationNumber":{"type":["string","null"],"example":"123"},"name":{"type":["string","null"]},"latitude":{"type":"number","example":52.52},"longitude":{"type":"number","example":13.405},"address":{"type":"object","properties":{"street":{"type":["string","null"]},"houseNumber":{"type":["string","null"]},"postalCode":{"type":["string","null"]},"city":{"type":["string","null"]}}},"distanceMeters":{"type":["number","null"],"example":820},"openingHours":{"type":["string","null"],"example":"24/7"},"source":{"type":"string","enum":["osm","manual"]}}},"SteuernummerValidationResult":{"type":"object","required":["input","valid","format","state","bundeseinheitlich","laenderspezifisch","finanzamt","reason"],"properties":{"input":{"type":"string","example":"101/815/08153"},"valid":{"type":"boolean"},"format":{"type":["string","null"],"enum":["laenderspezifisch","bundeseinheitlich",null]},"state":{"type":["object","null"],"properties":{"code":{"type":"string","example":"9"},"name":{"type":"string","example":"Bayern"}}},"bundeseinheitlich":{"type":["string","null"],"description":"13-digit unified form","example":"9101081508153"},"laenderspezifisch":{"type":["string","null"],"example":"101/815/08153"},"finanzamt":{"type":["object","null"],"properties":{"code":{"type":"string","example":"9101"},"name":{"type":"string","example":"Augsburg-Stadt"}}},"reason":{"type":["string","null"],"description":"machine-readable failure reason; null when valid","example":null}}},"PvYieldResult":{"type":"object","required":["location","system","available","annual","monthly"],"properties":{"location":{"type":"object","properties":{"lat":{"type":"number","example":52.52},"lng":{"type":"number","example":13.4}}},"system":{"type":"object","properties":{"kwp":{"type":"number","example":5},"lossPercent":{"type":"number","example":14},"tiltDeg":{"type":["number","null"],"example":35},"azimuthDeg":{"type":["number","null"],"description":"compass 0=N/90=E/180=S/270=W","example":180},"optimalAngles":{"type":"boolean"}}},"available":{"type":"boolean","description":"false when PVGIS could not be reached"},"annual":{"type":"object","properties":{"energyKwh":{"type":["number","null"],"example":5290},"specificYieldKwhPerKwp":{"type":["number","null"],"example":1058},"inPlaneIrradiationKwhPerM2Year":{"type":["number","null"],"example":1322}}},"monthly":{"type":"array","items":{"type":"object","required":["month","energyKwh"],"properties":{"month":{"type":"integer","minimum":1,"maximum":12},"energyKwh":{"type":"number"}}}}}},"BusinessDayAddResult":{"type":"object","required":["date","businessDays","state","result"],"properties":{"date":{"type":"string","example":"2026-01-02"},"businessDays":{"type":"integer","example":5},"state":{"type":["string","null"],"example":"BY"},"result":{"type":"string","example":"2026-01-09"}}},"BusinessDayCountResult":{"type":"object","required":["from","to","state","businessDays"],"properties":{"from":{"type":"string","example":"2026-01-05"},"to":{"type":"string","example":"2026-01-11"},"state":{"type":["string","null"],"example":"BY"},"businessDays":{"type":"integer","example":5}}},"SchulferienPeriod":{"type":"object","required":["name","startDate","endDate"],"properties":{"name":{"type":"string","example":"Sommerferien Bayern 2026"},"startDate":{"type":"string","format":"date","example":"2026-08-03"},"endDate":{"type":"string","format":"date","example":"2026-09-14"}}},"SchulferienResult":{"type":"object","required":["state","year","periods"],"properties":{"state":{"type":"string","example":"BY"},"year":{"type":"integer","example":2026},"periods":{"type":"array","items":{"$ref":"#/components/schemas/SchulferienPeriod"}}}},"SanctionsScreenRequest":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":200,"example":"John Smith"},"dateOfBirth":{"type":"string","format":"date","example":"1980-05-01"},"country":{"type":"string","minLength":2,"maxLength":2,"example":"DE"}}},"SanctionsHit":{"type":"object","required":["matchedName","score","subjectType","programme","euReferenceNumber","regulationSummary"],"properties":{"matchedName":{"type":"string","example":"Saddam Hussein Al-Tikriti"},"score":{"type":"number","format":"float","minimum":0,"maximum":1,"example":0.95},"subjectType":{"type":"string","example":"person"},"programme":{"type":["string","null"],"example":"IRQ"},"euReferenceNumber":{"type":["string","null"],"example":"EU.27.28"},"regulationSummary":{"type":["string","null"],"example":"1210/2003 (OJ L169)"}}},"SanctionsScreenResult":{"type":"object","required":["hits","listVersion"],"properties":{"hits":{"type":"array","items":{"$ref":"#/components/schemas/SanctionsHit"}},"listVersion":{"type":["string","null"],"example":"2026-06-05T15:51:25.849+02:00"}}},"MastrSolarInstallation":{"type":"object","required":["mastrNummer","postalCode","city","bundesland","latitude","longitude","grossCapacityKw","netCapacityKw","commissioningDate","operatingStatus","distanceMeters"],"properties":{"mastrNummer":{"type":"string","example":"SEE912345678"},"postalCode":{"type":["string","null"],"example":"39179"},"city":{"type":["string","null"],"example":"Sülztal"},"bundesland":{"type":["string","null"],"example":"Sachsen-Anhalt"},"latitude":{"type":"number","format":"float","example":52.012},"longitude":{"type":"number","format":"float","example":11.654},"grossCapacityKw":{"type":["number","null"],"format":"float","example":150.112},"netCapacityKw":{"type":["number","null"],"format":"float","example":148.987},"commissioningDate":{"type":["string","null"],"format":"date","example":"1998-01-02"},"operatingStatus":{"type":["integer","null"],"example":35},"distanceMeters":{"type":"integer","example":842}}},"MastrInstallationsResult":{"type":"object","required":["installations"],"properties":{"installations":{"type":"array","items":{"$ref":"#/components/schemas/MastrSolarInstallation"}}}},"EinvoiceInvoice":{"type":"object","description":"An EN16931-conformant invoice in the e-invoice-eu library's own internal UBL-tag-keyed format (ADR 0007) — this hub does not reshape it into a friendlier DTO, to avoid a custom-mapping correctness risk. Top-level key is `ubl:Invoice`; nested keys use the `cbc:`/`cac:` UBL prefixes, with `@`-suffixed sibling keys for XML attributes (e.g. `cbc:TaxAmount` + `cbc:TaxAmount@currencyID`). See the library's published `Invoice` TypeScript interface and `invoiceSchema` (Ajv 2019-09) for the full, authoritative shape: https://www.npmjs.com/package/@e-invoice-eu/core\n","additionalProperties":true},"ZugferdRequest":{"type":"object","required":["invoice","pdf"],"properties":{"invoice":{"$ref":"#/components/schemas/EinvoiceInvoice"},"pdf":{"type":"string","format":"byte","description":"Base64-encoded, already-rendered human-readable PDF representation of the invoice (the library embeds the CII XML into this PDF and adds PDF/A-3 conformance — it does not render an invoice layout from JSON alone). Render it however you like, including via this hub's own POST /api/v1/pdf/render.\n"}}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequest":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimited":{"description":"Per-key rate limit exceeded — either the per-minute burst ceiling (X-RateLimit-Limit/-Remaining response headers, Retry-After on this response) or, when the key has a daily quota configured, the requests/day cap (X-Daily-Quota-Limit/-Remaining headers). Each key's limits default to the deployment-wide settings unless overridden per key in the admin area.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"security":[{"sourceKey":[]}],"paths":{"/api/health":{"get":{"operationId":"getHealth","summary":"Liveness probe","security":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}}}}}}}},"/api/v1/geolocation/{postalCode}/cities":{"get":{"operationId":"getCities","summary":"Cities for a PLZ","parameters":[{"$ref":"#/components/parameters/postalCode"},{"name":"state","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cities for the PLZ","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CitiesResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/{postalCode}/cities/streets":{"get":{"operationId":"getStreets","summary":"Streets for a PLZ + city","parameters":[{"$ref":"#/components/parameters/postalCode"},{"name":"city","in":"query","required":false,"schema":{"type":"string"}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Street names","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pdf/fill":{"post":{"operationId":"pdfFill","summary":"Fill a PDF form's fields in-process (pdf-lib) and return the PDF","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["file","fields"],"properties":{"file":{"type":"string","description":"base64-encoded source PDF"},"flatten":{"type":"boolean","description":"flatten the form after filling"},"fields":{"type":"array","minItems":1,"items":{"type":"object","required":["type","key","value"],"properties":{"type":{"type":"string","enum":["text","radio","checkbox","signature","image","optionList","dropdown"]},"key":{"type":"string"},"value":{"description":"string/boolean; base64 image for image/signature"}}}}}}}}},"responses":{"200":{"description":"The filled PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"Invalid PDF or unknown field (PDF_FIELD_ERROR / INVALID_PDF)"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pdf/merge":{"post":{"operationId":"pdfMerge","summary":"Merge several base64 PDFs into one (in-process)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["files"],"properties":{"files":{"type":"array","minItems":2,"items":{"type":"string","description":"base64-encoded PDF"}}}}}}},"responses":{"200":{"description":"The merged PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"One of the inputs is not a valid PDF"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pdf/split":{"post":{"operationId":"pdfSplit","summary":"Split a base64 PDF per page or per page-range (in-process)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","description":"base64-encoded source PDF"},"ranges":{"type":"array","description":"[from, to] 1-based inclusive page ranges; omitted = one per page","items":{"type":"array","items":{"type":"integer","minimum":1},"minItems":2,"maxItems":2}}}}}}},"responses":{"200":{"description":"The resulting PDFs, base64-encoded","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"string","description":"base64 PDF"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"Invalid PDF or out-of-bounds range (PDF_RANGE_ERROR / INVALID_PDF)"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pdf/render":{"post":{"operationId":"pdfRender","summary":"Render a named server-side template + data payload to a PDF (in-process)","description":"Renders a registered document template to PDF bytes. The `data` payload is validated against the named template's own schema. Currently registered: `partner-contract` (Energiant partner framework contract — company master data + embedded signature PNG + contract version stamp).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["template","data"],"properties":{"template":{"type":"string","description":"Registered template name (e.g. partner-contract)"},"data":{"type":"object","description":"Template-specific payload, validated server-side","additionalProperties":true}}},"examples":{"partnerContract":{"summary":"partner-contract payload","value":{"template":"partner-contract","data":{"companyName":"Muster Solar GmbH","companyStreet":"Musterstraße 1","companyPostalCode":"10115","companyCity":"Berlin","contactName":"Erika Mustermann","vatId":"DE123456789","serviceArea":"Berlin + 50 km","signaturePng":"data:image/png;base64,iVBORw0KGgo...","signedAtIso":"2026-06-13"}}}}}}},"responses":{"200":{"description":"The rendered PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"Unknown template (PDF_TEMPLATE_UNKNOWN) or invalid render input (INVALID_PDF)"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/vat/validate":{"post":{"operationId":"validateVat","summary":"Validate an EU VAT-ID (local format + authoritative VIES lookup)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["vatId"],"properties":{"vatId":{"type":"string","example":"DE123456789"}}}}}},"responses":{"200":{"description":"Validation result. When VIES is unreachable, viesAvailable is false and only formatValid is authoritative.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["vatId","countryCode","formatValid","viesAvailable","registered"],"properties":{"vatId":{"type":"string","example":"DE123456789"},"countryCode":{"type":"string","example":"DE"},"formatValid":{"type":"boolean"},"viesAvailable":{"type":"boolean"},"registered":{"type":"boolean"},"name":{"type":["string","null"]},"address":{"type":["string","null"]}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"Malformed VAT-ID (error code INVALID_VAT_FORMAT)"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/email/validate":{"post":{"operationId":"validateEmail","summary":"Email lead-quality check (syntax + role + disposable + MX + SPF/DMARC)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","example":"info@example.com"},"probeCatchAll":{"type":"boolean","default":false,"description":"Opt-in: probe the domain for catch-all behaviour via live SMTP (slower, best-effort; unsolicited RCPT probing can be treated as abuse by receiving mail providers).\n"}}}}}},"responses":{"200":{"description":"Validation result. Malformed syntax returns 200 with syntaxValid false (the verdict is the payload). DNS/SMTP outages degrade the affected signal to null rather than failing the request.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["email","syntaxValid","role","disposable","deliverability"],"properties":{"email":{"type":"string","example":"info@example.com"},"syntaxValid":{"type":"boolean"},"localPart":{"type":["string","null"],"example":"info"},"domain":{"type":["string","null"],"example":"example.com"},"role":{"type":"boolean","description":"Generic mailbox (info@, kontakt@, …)"},"disposable":{"type":"boolean"},"mxFound":{"type":["boolean","null"]},"deliverability":{"type":"object","required":["spfFound","dmarcFound","dmarcPolicy","catchAll"],"properties":{"spfFound":{"type":["boolean","null"]},"dmarcFound":{"type":["boolean","null"]},"dmarcPolicy":{"type":["string","null"],"enum":["none","quarantine","reject",null]},"catchAll":{"type":["boolean","null"],"description":"Only populated when probeCatchAll was true."}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/phone/validate":{"post":{"operationId":"validatePhone","summary":"Validate a phone number (libphonenumber) — type, E.164, area code","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["phone"],"properties":{"phone":{"type":"string","example":"0171 1234567"},"region":{"type":"string","description":"ISO 3166-1 alpha-2 default region; defaults to DE","example":"DE"}}}}}},"responses":{"200":{"description":"Validation result. An unparseable number returns 200 with valid=false. areaCode is the Ortsnetzkennzahl (landline only), with the trunk 0 stripped.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["input","valid","type"],"properties":{"input":{"type":"string","example":"0171 1234567"},"valid":{"type":"boolean"},"formatted":{"type":["string","null"],"example":"+491711234567"},"type":{"type":"string","enum":["mobile","landline","voip","unknown"]},"regionCode":{"type":["string","null"],"example":"DE"},"areaCode":{"type":["string","null"],"example":"30"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/address/verify":{"post":{"operationId":"verifyAddress","summary":"Verify and normalize an address against the geo reference DB","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["postalCode","city"],"properties":{"street":{"type":"string","example":"Hauptstraße"},"postalCode":{"type":"string","example":"74921"},"city":{"type":"string","example":"Helmstadt"}}}}}},"responses":{"200":{"description":"Verification result. Casing is corrected to the canonical DB value where it matches; unresolved fields stay null. confidence is 0..1.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["plzValid","cityMatchesPlz","normalized","confidence"],"properties":{"plzValid":{"type":"boolean"},"cityMatchesPlz":{"type":"boolean"},"streetExistsForCity":{"type":["boolean","null"]},"normalized":{"type":"object","properties":{"street":{"type":["string","null"]},"postalCode":{"type":"string","example":"74921"},"city":{"type":["string","null"],"example":"Helmstadt"},"state":{"type":["string","null"],"example":"Baden-Württemberg"}}},"confidence":{"type":"number","format":"float","example":1}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geocode":{"get":{"operationId":"geocode","summary":"Forward-geocode an address to coordinates (geo DB / Nominatim)","parameters":[{"name":"street","in":"query","required":false,"schema":{"type":"string"},"example":"Hauptstraße"},{"name":"postalCode","in":"query","required":true,"schema":{"type":"string"},"example":"74921"},{"name":"city","in":"query","required":true,"schema":{"type":"string"},"example":"Helmstadt"}],"responses":{"200":{"description":"Coordinates with a source + confidence. A street yields street-level Nominatim precision; otherwise the PLZ centroid from the geo DB.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["found","confidence"],"properties":{"found":{"type":"boolean"},"latitude":{"type":["number","null"],"example":49.3},"longitude":{"type":["number","null"],"example":8.9},"confidence":{"type":"number","format":"float","example":0.9},"source":{"type":["string","null"],"enum":["geo-db","nominatim",null]},"state":{"type":["string","null"],"example":"Baden-Württemberg"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geocode/building":{"get":{"operationId":"geocodeBuilding","summary":"OSM building footprint (polygon + area) at a coordinate","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number"},"example":49.3},{"name":"lng","in":"query","required":true,"schema":{"type":"number"},"example":8.9}],"responses":{"200":{"description":"The building outline at the point (else the nearest within 30 m) plus its ground area. Degrades to found=false when no building is tagged.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["found"],"properties":{"found":{"type":"boolean"},"polygon":{"type":["object","null"],"description":"GeoJSON Polygon"},"areaSqm":{"type":["number","null"],"example":142},"osmWayId":{"type":["integer","null"],"example":42}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/lead/plausibility":{"post":{"operationId":"leadPlausibility","summary":"Anti-fraud / dedupe signals composing the contact validators","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","example":"info@example.com"},"phone":{"type":"string","example":"0171 2345678"},"region":{"type":"string","example":"DE"},"address":{"type":"object","required":["postalCode","city"],"properties":{"street":{"type":"string"},"postalCode":{"type":"string","example":"74921"},"city":{"type":"string","example":"Helmstadt"}}},"coordinates":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number","example":49.3},"lng":{"type":"number","example":8.9}}}}}}}},"responses":{"200":{"description":"Plausibility flags + a 0..100 score, plus SHA-256 fingerprints of the normalized contact fields the caller stores/compares to dedupe.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["score","flags","fingerprints"],"properties":{"score":{"type":"integer","minimum":0,"maximum":100,"example":80},"flags":{"type":"array","items":{"type":"object","required":["code","severity","message"],"properties":{"code":{"type":"string","example":"email_disposable"},"severity":{"type":"string","enum":["info","warning","critical"]},"message":{"type":"string"}}}},"fingerprints":{"type":"object","properties":{"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/lead/score":{"post":{"operationId":"leadScore","summary":"Composite lead score (blends contact validation, fraud, suitability)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Same contact fields as /lead/plausibility, plus an optional precomputed suitability hint (0..1) and per-channel weight overrides.\n","properties":{"email":{"type":"string"},"phone":{"type":"string"},"region":{"type":"string","example":"DE"},"address":{"type":"object","required":["postalCode","city"],"properties":{"street":{"type":"string"},"postalCode":{"type":"string","example":"74921"},"city":{"type":"string","example":"Helmstadt"}}},"coordinates":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}},"suitability":{"type":"number","minimum":0,"maximum":1,"example":0.8},"weights":{"type":"object","properties":{"email":{"type":"number"},"phone":{"type":"number"},"address":{"type":"number"},"suitability":{"type":"number"}}}}}}}},"responses":{"200":{"description":"Weighted 0..100 score, an A–D tier, reasons, and dedupe fingerprints.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["score","tier","reasons","fingerprints"],"properties":{"score":{"type":"integer","minimum":0,"maximum":100,"example":82},"tier":{"type":"string","enum":["A","B","C","D"]},"reasons":{"type":"array","items":{"type":"string"}},"fingerprints":{"type":"object","properties":{"email":{"type":["string","null"]},"phone":{"type":["string","null"]},"address":{"type":["string","null"]}}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/suitability/rooftop":{"post":{"operationId":"suitabilityRooftop","summary":"Rooftop-PV suitability (OSM roof + PVGIS irradiation) at a coordinate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number","example":49.3005},"lng":{"type":"number","example":8.9006}}}}}},"responses":{"200":{"description":"Installable-kWp / annual-yield estimate, a weighted score + verdict, and flagged data-availability gaps. Missing roof tags or a PVGIS outage degrade gracefully (verdict insufficient-data when no building).\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["location","building","irradiation","verdict","facts","flags","sources"],"properties":{"location":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}},"building":{"type":"object","properties":{"found":{"type":"boolean"},"footprintSqm":{"type":["number","null"]},"estimatedRoofSqm":{"type":["number","null"]},"levels":{"type":["integer","null"]},"roofShape":{"type":["string","null"]},"azimuthDeg":{"type":["number","null"]},"tiltDeg":{"type":["number","null"]},"tiltAssumed":{"type":"boolean"},"osmWayId":{"type":["integer","null"]}}},"irradiation":{"type":"object","properties":{"available":{"type":"boolean"},"inPlaneKwhPerM2Year":{"type":["number","null"]},"specificYieldKwhPerKwpYear":{"type":["number","null"]},"optimalSlopeDeg":{"type":["number","null"]}}},"estimatedKwp":{"type":["number","null"],"example":13.1},"estimatedAnnualKwh":{"type":["number","null"],"example":14600},"score":{"type":["integer","null"],"example":78},"verdict":{"type":"string","enum":["suitable","conditional","unsuitable","insufficient-data"]},"facts":{"type":"array","items":{"type":"string"}},"flags":{"type":"array","items":{"type":"string"}},"sources":{"type":"array","items":{"type":"string"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/parcel":{"post":{"operationId":"resolveParcel","summary":"Resolve the ALKIS Flurstück (parcel geometry + area) at a coordinate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lat","lng"],"properties":{"lat":{"type":"number","example":48.778},"lng":{"type":"number","example":9.18}}}}}},"responses":{"200":{"description":"The containing parcel's geometry + official area from the detected state's open ALKIS WFS (BW/BE/SN/NRW). States without an open WFS (e.g. Bayern, token-gated) and lookup outages return found=false with a reason.\n","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["found"],"properties":{"found":{"type":"boolean"},"areaSqm":{"type":["number","null"],"example":3419},"geometry":{"type":["object","null"],"description":"GeoJSON Polygon/MultiPolygon (WGS84)"},"source":{"type":["string","null"],"example":"alkis-bw"},"state":{"type":["string","null"],"example":"Baden-Württemberg"},"reason":{"type":["string","null"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/holidays/{year}":{"get":{"operationId":"getHolidays","summary":"German public holidays for a year, optionally per federal state","parameters":[{"name":"year","in":"path","required":true,"schema":{"type":"integer","minimum":1970,"maximum":2200},"example":2024},{"name":"state","in":"query","required":false,"description":"ISO 3166-2:DE code (e.g. BY). Omitted = nationwide holidays only.","schema":{"type":"string","enum":["BW","BY","BE","BB","HB","HH","HE","MV","NI","NW","RP","SL","SN","ST","SH","TH"]}}],"responses":{"200":{"description":"Holidays for the year, sorted by date","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","required":["date","name","nationwide"],"properties":{"date":{"type":"string","format":"date","example":"2024-01-01"},"name":{"type":"string","example":"Neujahr"},"nationwide":{"type":"boolean"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/suitability/assess":{"post":{"operationId":"assessSuitability","summary":"Assess a parcel's ground-mount PV suitability (open-field PV)","description":"Returns a suitability assessment for a parcel — identified by a drawn GeoJSON polygon (preferred: real area + zone intersection), by lat+lng, or by PLZ — combining grid proximity, protected-area exclusions, the EEG support zone (benachteiligtes Gebiet + 200 m corridor), and parcel size into a 0–100 `score` and a `verdict`. A hard exclusion disqualifies outright (`unsuitable`); otherwise the layers are blended (soft exclusions penalized) into suitable/conditional/unsuitable, or `insufficient-data` when no location layer is available. Per-state EEG/ALKIS coverage is added in tranches. See docs/adr/0001-pv-suitability-data-sources.md.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"postalCode":{"type":"string","pattern":"^\\d{5}$"},"minSizeSqm":{"type":"number","exclusiveMinimum":0,"description":"parcel size in m²"},"geometry":{"type":"object","description":"Drawn parcel as a GeoJSON Polygon/MultiPolygon ([lng, lat] positions). When given, area is computed from it and the zone layers use polygon intersection; lat/lng/PLZ/size are ignored.","required":["type","coordinates"],"properties":{"type":{"type":"string","enum":["Polygon","MultiPolygon"]},"coordinates":{"type":"array","items":{}}}}},"description":"Provide a drawn geometry, lat+lng together, or a postalCode."},"examples":{"byCoordinates":{"summary":"by coordinates","value":{"lat":51.34,"lng":12.37,"minSizeSqm":40000}},"byPostalCode":{"summary":"by PLZ","value":{"postalCode":"04109"}}}}}},"responses":{"200":{"description":"The suitability assessment","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["location","verdict","facts","sources"],"properties":{"location":{"type":"object","properties":{"lat":{"type":["number","null"]},"lng":{"type":["number","null"]},"postalCode":{"type":["string","null"]}}},"parcel":{"type":["object","null"]},"grid":{"type":["object","null"]},"eeg":{"type":["object","null"]},"exclusions":{"type":["array","null"],"items":{"type":"object"}},"score":{"type":["number","null"]},"verdict":{"type":"string","enum":["suitable","conditional","unsuitable","insufficient-data"]},"facts":{"type":"array","items":{"type":"string"}},"sources":{"type":"array","items":{"type":"string"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/meta":{"get":{"operationId":"getMeta","summary":"Capabilities — modules, scopes, endpoints, and what this key may call","responses":{"200":{"description":"The module/scope catalogue with per-scope grant flags","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["modules","grantedScopes","rateLimit"],"properties":{"grantedScopes":{"type":"array","items":{"type":"string"}},"rateLimit":{"type":"object","required":["max","dailyQuota"],"description":"The presented key's own rate-limit tier. `max` is the per-minute burst ceiling (the key's override, or the deployment default when null); `dailyQuota` is the requests/day cap, or null when uncapped.\n","properties":{"max":{"type":["integer","null"],"example":120},"dailyQuota":{"type":["integer","null"],"example":null}}},"modules":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"label":{"type":"string"},"scopes":{"type":"array","items":{"type":"object","properties":{"scope":{"type":"string"},"label":{"type":"string"},"granted":{"type":"boolean"},"endpoints":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"}}}}}}}}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api/v1/bank/codes/{blz}":{"get":{"operationId":"getBankByCode","summary":"Resolve a German Bankleitzahl to its bank name and BIC","parameters":[{"name":"blz","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{8}$"},"example":"37040044"}],"responses":{"200":{"description":"The bank for the Bankleitzahl","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/BankCode"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No bank with this Bankleitzahl"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/search":{"get":{"operationId":"searchCities","summary":"Autocomplete over PLZ + city (prefix on PLZ, substring on city)","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"example":"101"}],"responses":{"200":{"description":"Matching PLZ-cities (capped for typeahead)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","required":["postalCode","name","state"],"properties":{"postalCode":{"type":"string","example":"10115"},"name":{"type":"string","example":"Berlin"},"state":{"type":"string","example":"Berlin"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/{postalCode}/coordinates":{"get":{"operationId":"getPostalCodeCoordinates","summary":"Centroid coordinate of a PLZ (optionally a specific city)","parameters":[{"$ref":"#/components/parameters/postalCode"},{"name":"city","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Coordinate for the PLZ","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["postalCode","city","latitude","longitude"],"properties":{"postalCode":{"type":"string","example":"10115"},"city":{"type":"string","example":"Berlin"},"latitude":{"type":"number","example":52.5321},"longitude":{"type":"number","example":13.3849}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No coordinates for this PLZ"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/distance/batch":{"post":{"operationId":"batchDistances","summary":"Distance from one base PLZ to many target PLZ in a single request","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["basePostalCode","targets"],"properties":{"basePostalCode":{"type":"string","pattern":"^\\d{5}$","example":"10115"},"baseCity":{"type":"string"},"targets":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","pattern":"^\\d{5}$"}}}}}}},"responses":{"200":{"description":"Distance (km) to each target PLZ; null when a target has no coordinate","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["basePostalCode","count","results"],"properties":{"basePostalCode":{"type":"string"},"count":{"type":"integer"},"results":{"type":"array","items":{"type":"object","required":["postalCode","city","distance"],"properties":{"postalCode":{"type":"string"},"city":{"type":["string","null"]},"distance":{"type":["number","null"],"description":"kilometres"}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/radius":{"get":{"operationId":"getCitiesNearCoordinate","summary":"Reverse radius — PLZ-cities near a latitude/longitude, nearest first","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number","minimum":-90,"maximum":90},"example":52.5321},{"name":"lng","in":"query","required":true,"schema":{"type":"number","minimum":-180,"maximum":180},"example":13.3849},{"name":"radius","in":"query","required":true,"description":"radius in metres","schema":{"type":"integer","minimum":1,"maximum":500000}}],"responses":{"200":{"description":"Cities within range of the coordinate","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RangeResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/{postalCode}/cities/radius/{radius}":{"get":{"operationId":"getCitiesInRadius","summary":"Cities within a radius (metres), sorted by distance","parameters":[{"$ref":"#/components/parameters/postalCode"},{"name":"radius","in":"path","required":true,"schema":{"type":"integer","minimum":1,"maximum":500000}},{"name":"baseCity","in":"query","required":false,"schema":{"type":"string"}},{"name":"city","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cities within range","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RangeResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/finance/tax-offices":{"get":{"operationId":"getTaxOffices","summary":"All German tax offices (Finanzämter)","responses":{"200":{"description":"Tax offices ordered by state then name","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TaxOffice"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/finance/tax-offices/{stateCode}":{"get":{"operationId":"getTaxOfficesByState","summary":"Tax offices of one federal state","parameters":[{"name":"stateCode","in":"path","required":true,"description":"Federal-state code 1–16 (official Bundesland ordering)","schema":{"type":"string","pattern":"^\\d{1,2}$"},"example":"9"}],"responses":{"200":{"description":"Tax offices of the state, ordered by name","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TaxOffice"}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bank/iban/validate":{"post":{"operationId":"validateIban","summary":"Validate an IBAN (ISO 13616) and enrich German ones with bank + BIC","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["iban"],"properties":{"iban":{"type":"string","example":"DE89 3704 0044 0532 0130 00"}}}}}},"responses":{"200":{"description":"The IBAN is valid","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IbanValidationResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"The IBAN failed validation (error code INVALID_IBAN)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bank/iban/derive":{"post":{"operationId":"deriveIban","summary":"Derive a German IBAN from a Bankleitzahl + Kontonummer (Bundesbank standard rule)","description":"Derives the IBAN using the Bundesbank standard rule (Kontonummer zero-padded to 10 digits + BLZ, ISO 13616 check digits). The BLZ must be a known institute in the reference data. Coverage caveat: ~60 institutes use non-standard Bundesbank \"Sonderregeln\" not implemented here (their rule dataset is gated, not openly available) — verify the result against the bank for payouts of consequence.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["blz","kontonummer"],"properties":{"blz":{"type":"string","pattern":"^\\d{8}$","example":"37040044"},"kontonummer":{"type":"string","pattern":"^\\d{1,10}$","example":"532013000"}}}}}},"responses":{"200":{"description":"The derived IBAN.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IbanDerivationResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"description":"The BLZ is not a known institute (error code UNKNOWN_BLZ)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/geolocation/{postalCode}/distance/{basePostalCode}/cities":{"get":{"operationId":"getDistances","summary":"Distance from a base PLZ-city to each city of the PLZ","parameters":[{"$ref":"#/components/parameters/postalCode"},{"name":"basePostalCode","in":"path","required":true,"schema":{"type":"string","pattern":"^\\d{5}$"}},{"name":"baseCity","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Distances to the target PLZ cities","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RangeResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/sepa/credit-transfer":{"post":{"operationId":"generateSepaCreditTransfer","summary":"Generate a SEPA credit-transfer file (ISO 20022 pain.001.001.03)","description":"Builds a pain.001 credit-transfer XML from a debtor plus a batch of payouts. Pure generation — nothing is persisted. messageId and createdAt default server-side when omitted. Amounts are integer cents. BIC is optional (IBAN-only / \"BIC-less\" transfers are supported).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requestedExecutionDate","debtor","payments"],"properties":{"messageId":{"type":"string","maxLength":35,"description":"Unique file id; generated server-side when omitted."},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp (ISO 8601); defaults to now."},"requestedExecutionDate":{"type":"string","format":"date","example":"2026-07-01"},"debtor":{"$ref":"#/components/schemas/SepaParty"},"payments":{"type":"array","minItems":1,"maxItems":10000,"items":{"$ref":"#/components/schemas/SepaPayment"}}}}}}},"responses":{"200":{"description":"The generated credit-transfer file plus reconciliation totals.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["messageId","transactionCount","controlSum","xml"],"properties":{"messageId":{"type":"string"},"transactionCount":{"type":"integer","example":3},"controlSum":{"type":"string","example":"370.45"},"xml":{"type":"string","description":"pain.001.001.03 XML"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/sepa/direct-debit":{"post":{"operationId":"generateSepaDirectDebit","summary":"Generate a SEPA direct-debit collection file (ISO 20022 pain.008.001.02)","description":"Builds a pain.008 direct-debit-collection XML from a creditor plus a batch of mandated debits. Pure generation — nothing is persisted. messageId and createdAt default server-side when omitted. Transactions are grouped into one payment-information block per mandate sequence type (FRST/RCUR/OOFF/FNAL), per the schema. Amounts are integer cents.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requestedCollectionDate","creditor","transactions"],"properties":{"messageId":{"type":"string","maxLength":35,"description":"Unique file id; generated server-side when omitted."},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp (ISO 8601); defaults to now."},"requestedCollectionDate":{"type":"string","format":"date","example":"2026-07-15"},"creditor":{"$ref":"#/components/schemas/SepaDirectDebitCreditor"},"transactions":{"type":"array","minItems":1,"maxItems":10000,"items":{"$ref":"#/components/schemas/SepaDirectDebitTransaction"}}}}}}},"responses":{"200":{"description":"The generated direct-debit file plus reconciliation totals.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["messageId","transactionCount","controlSum","xml"],"properties":{"messageId":{"type":"string"},"transactionCount":{"type":"integer","example":3},"controlSum":{"type":"string","example":"149.70"},"xml":{"type":"string","description":"pain.008.001.02 XML"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/extraction/document":{"post":{"operationId":"extractDocument","summary":"Extract structured fields from a document (provider-agnostic)","description":"Given a base64 document (PDF or image) plus a requested field set, returns each field with a value, confidence and an explicit needsReview flag (human-in-the-loop). Provider-agnostic; when no AI provider is configured the stub adapter returns every field as needsReview. GDPR: the document is processed in memory and never persisted — only the extracted fields are returned, and no document bytes or values are logged.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mimeType","document","fields"],"properties":{"mimeType":{"type":"string","enum":["application/pdf","image/png","image/jpeg","image/webp"]},"document":{"type":"string","description":"Base64-encoded document bytes (max 10 MB decoded)."},"fields":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/ExtractionFieldSpec"}}}}}}},"responses":{"200":{"description":"The extracted fields keyed by the requested field keys.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DocumentExtractionResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"extraction\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pdf/detect-fields":{"post":{"operationId":"detectPdfFields","summary":"Detect fillable field positions in a flat PDF form","description":"Locates the fillable fields in a flat PDF — underscore input lines, ☐ checkboxes, and label-anchored comb fields (IBAN/Zählernummer/…) — and returns their exact positions in PDF points (bottom-left origin) so an editor or pdf-lib can place AcroForm widgets there. Positions come from the PDF text layer; vector-only fields (signature areas, vector checkboxes) are a follow-up vision pass. The document is processed in memory and never persisted.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["document"],"properties":{"document":{"type":"string","description":"Base64-encoded PDF (max 20 MB decoded)."},"mode":{"type":"string","enum":["text","ai"],"default":"text","description":"text = exact text-layer detection only (free); ai = also run the Mistral OCR generalizer for arbitrary/box forms (signature + table fields, approximate; paid OCR call).\n"}}}}}},"responses":{"200":{"description":"The detected fields with positions (exact + optionally approximate).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/FormDetectionResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"form-detect\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/parcel-locker/nearby":{"get":{"operationId":"parcelLockerNearby","summary":"Nearest parcel lockers for a coordinate or PLZ (provider-agnostic, OSM/ODbL)","description":"Returns parcel lockers (DHL Packstation, Amazon Locker, …) near a coordinate (lat+lng) or a PLZ, sorted by distance. Provider is explicit (carrier coupling): pass `provider` to restrict to the carrier you ship with. Data is OSM-derived (ODbL); `meta.attribution` carries the required \"© OpenStreetMap-Mitwirkende\" credit. The data layer is built incrementally — until it lands the endpoint returns an empty result set.\n","parameters":[{"name":"lat","in":"query","required":false,"schema":{"type":"number"},"example":52.52},{"name":"lng","in":"query","required":false,"schema":{"type":"number"},"example":13.405},{"name":"plz","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{5}$"},"example":"10115"},{"name":"radius","in":"query","required":false,"description":"Search radius in meters (default 10000, max 50000).","schema":{"type":"integer","maximum":50000}},{"name":"provider","in":"query","required":false,"description":"CSV carrier filter, e.g. dhl,amazon.","schema":{"type":"string"},"example":"dhl"},{"name":"limit","in":"query","required":false,"description":"Max results (default 20, max 100).","schema":{"type":"integer","maximum":100}}],"responses":{"200":{"description":"Stations sorted by distance, with ODbL attribution + count in meta.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["stations"],"properties":{"stations":{"type":"array","items":{"$ref":"#/components/schemas/ParcelLockerStation"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"parcel-locker\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/steuernummer/validate":{"post":{"operationId":"validateSteuernummer","summary":"Validate + convert a German Steuernummer (state form ↔ unified 13-digit)","description":"Validates a German Steuernummer and converts between the state-specific form (10–11 digits) and the unified federal ELSTER form (13 digits). A 13-digit input is self-describing; a 10/11-digit input needs `state` (federal-state code 1–16 or German state name). The embedded Bundesfinanzamtsnummer is cross-checked against the Finanzamt reference data. Fully deterministic, no external service. The Prüfziffer is not algorithmically verified (no public uniform algorithm).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["steuernummer"],"properties":{"steuernummer":{"type":"string","description":"With or without separators (/, spaces).","example":"101/815/08153"},"state":{"type":"string","description":"Federal-state code (1–16) or German state name; required for the state form.","example":"Bayern"}}}}}},"responses":{"200":{"description":"The validation verdict plus both representations and the resolved Finanzamt.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SteuernummerValidationResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"steuernummer\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/pv/yield":{"get":{"operationId":"pvYield","summary":"Expected PV yield (annual + monthly) for a coordinate and system (PVGIS)","description":"Returns the expected photovoltaic yield for a location and system from PVGIS. Provide `tilt` + `azimuth` for a fixed mounting, or omit them (or pass `optimal=true`) to let PVGIS optimize the angles. Azimuth uses the compass convention (0=N, 90=E, 180=S, 270=W). A PVGIS outage degrades to `available: false` rather than failing. PVGIS © European Union.\n","parameters":[{"name":"lat","in":"query","required":true,"schema":{"type":"number"},"example":52.52},{"name":"lng","in":"query","required":true,"schema":{"type":"number"},"example":13.4},{"name":"kwp","in":"query","required":false,"description":"Installed peak power (default 1).","schema":{"type":"number","maximum":10000}},{"name":"loss","in":"query","required":false,"description":"System loss percentage (default 14).","schema":{"type":"number","minimum":0,"maximum":100}},{"name":"tilt","in":"query","required":false,"description":"Module tilt in degrees; omit for optimized angles.","schema":{"type":"number","minimum":0,"maximum":90}},{"name":"azimuth","in":"query","required":false,"description":"Compass azimuth 0–360 (0=N, 180=S).","schema":{"type":"number","minimum":0,"maximum":360}},{"name":"optimal","in":"query","required":false,"description":"true/1 to force PVGIS-optimized tilt + azimuth.","schema":{"type":"string"}}],"responses":{"200":{"description":"The yield estimate, with PVGIS attribution in meta.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/PvYieldResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"pv-yield\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/business-days/add":{"get":{"operationId":"businessDaysAdd","summary":"Add/subtract working days to a date (deadline calculation)","description":"Adds `days` working days to `date` (negative counts backwards), skipping weekends and the public holidays of the given Bundesland (nationwide-only without `state`). Deterministic.\n","parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-01-02"},{"name":"days","in":"query","required":true,"description":"Working-day offset (negative counts backwards).","schema":{"type":"integer","minimum":-3650,"maximum":3650},"example":5},{"name":"state","in":"query","required":false,"description":"ISO 3166-2:DE state code; omit for nationwide holidays only.","schema":{"type":"string"},"example":"BY"}],"responses":{"200":{"description":"The resulting working day.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BusinessDayAddResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"business-days\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/business-days/count":{"get":{"operationId":"businessDaysCount","summary":"Count working days in an inclusive date range","description":"Counts the working days in [from, to] (both ends inclusive), skipping weekends and the public holidays of the given Bundesland (nationwide-only without `state`). `from` must be on or before `to`. Deterministic.\n","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-01-05"},{"name":"to","in":"query","required":true,"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"example":"2026-01-11"},{"name":"state","in":"query","required":false,"schema":{"type":"string"},"example":"BY"}],"responses":{"200":{"description":"The number of working days in the range.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/BusinessDayCountResult"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"business-days\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/schulferien":{"get":{"operationId":"getSchulferien","summary":"School holidays (Schulferien) for a Bundesland + year","description":"Returns the Schulferien periods for the given state + year. Coverage varies per state/year; an uncovered combination returns an empty `periods` array, not an error. Data is community-maintained (paulbrejla/ferien-api-data, MIT) — \"ohne Gewähr\", no official government source — distinct from the deterministic, authoritative public-holiday computation in the `holidays` scope. See meta.attribution on the response.\n","parameters":[{"name":"state","in":"query","required":true,"schema":{"type":"string"},"example":"BY"},{"name":"year","in":"query","required":true,"schema":{"type":"integer"},"example":2026}],"responses":{"200":{"description":"The Schulferien periods for the state + year.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SchulferienResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"schulferien\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/sanctions/screen":{"post":{"operationId":"sanctionsScreen","summary":"Fuzzy-screen a name against the EU consolidated financial sanctions list","description":"Automated fuzzy-name screening against the EU Financial Sanctions Files (FSF) consolidated list (ADR 0005) — not a legal compliance determination; review hits manually before acting on them. An empty `hits` array is a valid no-match result, not an error. Optional `dateOfBirth`/`country` only boost the score of an agreeing name match — they never filter out a name match on their own.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SanctionsScreenRequest"}}}},"responses":{"200":{"description":"Fuzzy-matched sanctions list hits, ordered by score descending.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/SanctionsScreenResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"sanctions\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/mastr/installations":{"get":{"operationId":"mastrInstallations","summary":"Nearby MaStR PV installations for a coordinate or PLZ","description":"Returns PV installations from the Bundesnetzagentur Marktstammdatenregister (MaStR, ADR 0006) near the given coordinate or PLZ, sorted by distance. Either lat+lng or plz is required. Installations without published coordinates are not included (coordinate-radius search only). DL-DE-BY-2.0 licensed — see meta.attribution.\n","parameters":[{"name":"lat","in":"query","schema":{"type":"number","minimum":-90,"maximum":90},"example":52.012},{"name":"lng","in":"query","schema":{"type":"number","minimum":-180,"maximum":180},"example":11.654},{"name":"plz","in":"query","schema":{"type":"string","pattern":"^\\d{5}$"},"example":"39179"},{"name":"radius","in":"query","schema":{"type":"integer","default":5000,"maximum":50000},"description":"meters"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Nearby PV installations, ordered by distance ascending.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/MastrInstallationsResult"},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"mastr\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/einvoice/xrechnung":{"post":{"operationId":"einvoiceXrechnung","summary":"Generate a standalone XRechnung (UBL) XML document","description":"Generates an EN16931-conformant XRechnung UBL XML document from an invoice (ADR 0007). Returns the raw XML document, not the `{ data, meta }` envelope (matching this hub's binary-file-response convention from the pdf module).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EinvoiceInvoice"}}}},"responses":{"200":{"description":"The generated XRechnung UBL XML document.","content":{"application/xml":{"schema":{"type":"string"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"einvoice\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/einvoice/zugferd":{"post":{"operationId":"einvoiceZugferd","summary":"Generate a Factur-X/ZUGFeRD PDF/A-3 with embedded CII XML","description":"Generates a Factur-X/ZUGFeRD PDF/A-3 (EN16931 \"Comfort\" profile, ADR 0007) by embedding the invoice's CII XML into a caller-supplied, already-rendered human-readable PDF. Returns the raw PDF, not the `{ data, meta }` envelope.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZugferdRequest"}}}},"responses":{"200":{"description":"The generated Factur-X PDF/A-3.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"The API key lacks the \"einvoice\" scope"},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}